Skip to content

Conversation

@tbagrel1
Copy link
Contributor

Description

Please include a meaningful description of the PR and link the relevant issues
this PR might resolve.

Also note that:

  • New code should be properly tested (even if it does not add new features).
  • The fix for a regression should include a test that reproduces said regression.

@tbagrel1 tbagrel1 added the peras label Nov 20, 2025
@agustinmista agustinmista changed the base branch from main to peras-staging November 20, 2025 18:22
@agustinmista
Copy link
Contributor

There's an implementation detail that we should discuss with the researchers: whether or not we need to store votes targeting a different block than the one a node is currently championing.

  • If we store every vote we receive (from within the voting committee), then an attacked with many low-stake nodes that make it into the committee could cause a honest node to store an unreasonable amount of bogus votes for different blocks.
  • On the other hand, if we only keep track of votes being cast upon the block we are currently championing, then we would be unable to ever observe a quorum by ourselves, relying instead on certificate diffusion to reconcile the global state.

If an honest node is voting for the wrong block because of adversarial actions, is it realistic for it to expect receiving votes from the honest majority? Or, conversely, to receive a certificate when a quorum is reached elsewhere.

Copy link
Contributor

@agustinmista agustinmista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good so far! I have some thoughts around what we discussed the last time. Maybe they could lead to some simplifications.

Also, would you mind marking the new TODOs somehow? Like TODO(peras) so that we can easily grep for them.

PerasVoteStakeDistr ->
Either (PerasValidationErr blk) (ValidatedPerasVote blk)

const_PERAS_QUORUM_THRESHOLD :: PerasVoteStake
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if it's not a tunable parameter, I think it would be good to move this to Ouroboros.Consensus.Peras.Params

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I'm not sure that the part of the code dealing with the PerasVoteDB (probably the ChainDB, as PerasCertDB is managed as a part of the ChainDB already) will have access to those params?

If we follow the same structure as for the PerasCertDB, we will have a PerasCertDBArgs struct as a parameter of openPerasVoteDB that could be used to host this param.

Comment on lines 227 to 232
data PerasVoteAggregateStatus blk
= PerasVoteAggregateQuorumNotReached {pvasVoteAggregate :: !(PerasVoteAggregate blk)}
| PerasVoteAggregateQuorumReachedAlready
{pvasVoteAggregate :: !(PerasVoteAggregate blk), pvasCert :: ValidatedPerasCert blk}
deriving stock (Generic, Eq, Ord, Show)
deriving anyclass NoThunks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this way better!

What I am still a bit concerned about, though, is that complete and incomplete voting aggregates are still represented by the same type, and that could make it easier to introduce bugs in the future.

Maybe we can define a GADT indexed by the voting state to make these two cases different at the type level, while still allowing for a single type to be carried around in most places:

type data PerasVotingStatus = NoQuorum | Quorum

data PerasVotingTally blk status where
  NoQuorum ::
    !(PerasVoteAggregate) -> 
    PerasVotingTally NoQuorum
  Quorum :: 
    !(PerasVoteAggregate blk) ->
    !(ValidatedPerasCert blk) ->
    PerasVotingTally Quorum

This way, we could enforce the voting code to only accept the expected state transitions:

  • NoQuorum -> NoQuorum
  • NoQuorum -> Quorum
  • Quorum -> Quorum
    (note that Quorum -> NoQuorum shouldn't be possible)

As well as allowing future client code using these aggregates to specify when it expects voting to have reached a quorum, as opposed to handling it dynamically via a Maybe and/or some error "impossible case". E.g., instead of pvasMaybeCert, we could have:

perasVotingCert :: PerasVotingTally blk Quorum -> ValidatedPerasCert blk
perasVotingCert (Quorum _ cert) = cert

Which can only be used to extract the cert from the aggregate bundle after voting has reached a quorum.


Going one step further, we could also encode the number of votes that we have seen after reaching a quorum in the type-level Quorum constructor:

type data PerasVotingStatus = NoQuorum | Quorum Nat

This would allow us to enrich the state transitions to:

  • NoQuorum -> NoQuorum
  • NoQuorum -> Quorum Z
  • Quorum n -> Quorum (S n)

And we could use Quorum 0 as the witness for the state transition that produces a certificate. This way, I think we could able to get rid of the PerasVoteResult type* or, alternatively, turn it into a thin wrapper over PerasVotingTally to hide the existential:

data PerasVotingTally blk status where
  NoQuorum ::
    !(PerasVoteAggregate) -> 
    PerasVotingTally NoQuorum
  Quorum ::
    !(SNat n) ->
    !(PerasVoteAggregate blk) ->
    !(ValidatedPerasCert blk) ->
    PerasVotingTally (Quorum n)

-- constructor not exported
newtype PerasVoteResult blk = PerasVoteResult (Some (PerasVoteTally blk))

-- instead we could expose only the things the client need, e.g.:
-- to extract the certificate if we have _just_ reached a quorum, and never again
perasVoteResultEmittedCert :: PerasVoteResult blk -> Maybe (ValidatedPerasCert blk)
perasVoteResultEmittedCert (PerasVoteResult (Some tally)) =
  case tally of
    Quorum Z _ cert -> Just cert
    _ -> Nothing

[*] depending on whether the future client code might also be interested in reacting to the event of adding a certificate that was already in the VoteDB (I think it wouldn't, as that's sounds as something purely relevant to tracing, which is already being handled by the VoteDB implementation)

, pvaTotalStake = initialStake
}
vote =
if not (getPerasVoteRound vote == roundNo && getPerasVoteVotedBlock vote == point)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would apply DeMorgan's law to this condition:

if getPerasVoteRound vote /= roundNo || getPerasVoteVotedBlock vote /= point


-- | Add a vote to an existing aggregate if it isn't already present, and update
-- the stake accordingly.
-- PRECONDITION: the vote's target must match the aggregate's target.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is only being used by updatePerasVoteAggregateStatus. Does the latter also inherit this precondition? If not, I would then embed this function as a local binding to updatePerasVoteAggregateStatus and get rid of the error. Otherwise, updatePerasVoteAggregateStatus should also mention the precondition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants