Add handling for Auxiliary info#402
Conversation
5fd18af to
98a5139
Compare
Introduce an AuxiliaryInfoApp interface abstraction that lets an application
piggyback application-specific data on Simplex epoch changes (e.g.
threshold distributed key generation), and gate the epoch transition on
that data being final. Final doesn't mean finalized as in Simplex,
but rather that the data is "good enough" to be used for the next epoch.
Metadata & encoding:
- Add AuxiliaryInfo (Info, PrevAuxInfoSeq, ApplicationID) to block
metadata and an AppID type, with Canoto (de)serialization.
- Each block's PrevAuxInfoSeq back-points to the most recent block with a
non-empty Info, skipping empty-Info blocks. collectAuxiliaryInfo walks
these pointers to rebuild the epoch's aux info history.
The digest of the last AuxInfo in a final history is signed by the node,
rather than the entire history of non-empty Aux Info.
This is done for flexibility, as some applications like publicly verifiable DKG
have commutative histories.
State machine:
- AuxiliaryInfoApp drives the flow: GenerateAuxInfo contributes to the
history until IsFinalAuxInfoHistory reports it ready; only then are
next-epoch approvals collected. IsLegalAuxInfoAppend validates a
proposed append on the verify path. Each method also receives the
next epoch's validator set (NodeBLSMappings).
- The builder carries forward / extends aux info, and the verifier
reconstructs the expected AuxiliaryInfo (ApplicationID, PrevAuxInfoSeq)
and enforces it via the block-digest comparison.
Approvals:
- Approvals now bind to the auxiliary info: the signed payload is
(NextPChainReferenceHeight, auxInfoDigest), where auxInfoDigest is the
sha256 of the last aux info element in the history.
- The ApprovalStore is now keyed by (NodeID, PChainHeight, AuxInfoSeqDigest)
so approvals for different digests coexist, and sanitizeApprovals only
aggregates approvals matching the candidate height and digest.
Tests:
- Verify approvals with real ECDSA signatures over the signed payload
(the test signature verifier now actually checks signatures and
(NextPChainReferenceHeight, auxInfoDigest), where auxInfoDigest is the
sha256 of the final aux info history. assembleApprovalToBeSigned
replaces the previous height-only payload.
The switch to ECDSA is because this way we make sure we really sign
the correct digest and not an empty one because of a bug.
- The ApprovalStore is keyed by (NodeID, PChainHeight, AuxInfoSeqDigest)
so approvals for different digests coexist, and sanitizeApprovals only
aggregates approvals matching the candidate height and digest.
Signed-off-by: Yacov Manevich <yacov.manevich@avalabs.org>
| if len(auxInfo.Info) > 0 { | ||
| history = append(history, auxInfo.Info) | ||
| seqs = append(seqs, currentSeq) | ||
| appID = auxInfo.ApplicationID |
There was a problem hiding this comment.
im not sure I understand application id. I'm assuming it helps the user of AuxInfo to decode the info bytes. So for example DKG would have an application id of 1, and then we would decode all []info bytes in the history message with application id 1 a certain way.
Can't we have multiple application running though? Maybe a DKG and something else in the future so we would have multiple application ids corresponding within the same epoch?
There was a problem hiding this comment.
or are you saying if we have multiple applications, they would both be serialized into a single info []byte? in this case the application id is just used for versioning of how to deserialize the bytes?
There was a problem hiding this comment.
Suppose we implement DKG in avalanchego version v1.16.1 and then in version v1.16.2 we improve the DKG implementation or fix a bug that requires changing the encoding.
A node that is mid epoch change and is restarted from v1.16.1 to v1.16.2 needs to continue using the old DKG implementation until the epoch change ends. It cannot use the new implementation mid epoch change.
The application ID is there to ensure every epoch change uses a uniform version of DKG implementation.
| var history [][]byte | ||
| // seqs[i] is the block sequence that history[i] was collected from, so the caller can point a | ||
| // new block's PrevAuxInfoSeq at the most recent non-empty Info block (the last item, after reversing). | ||
| var seqs []uint64 |
There was a problem hiding this comment.
do you think it would be easier to reason through if we created a struct
{
[]byte
seq
}
rather than maintaining two separate arrays? it would also ensure the sorting is done correctly for the history since we can just sort by seq in this new struct
There was a problem hiding this comment.
yeah we can do that. Initially I only had the history and then added the seqs. Will change
|
|
||
| // collectAuxiliaryInfo traverses backwards starting from the given block and collects the AuxiliaryInfo of all blocks in the chain. | ||
| // returns the collected AuxiliaryInfo, the corresponding sequences of the blocks they were collected from, | ||
| // and the application ID of the most recent non-empty Info block (or defaultAppID if there was none). |
There was a problem hiding this comment.
i think the applicationID returned is actually the oldest
There was a problem hiding this comment.
yeah, you're right and that's by design. Will fix
| type approvalsByPChainHeight map[uint64]*approvalAndTimestamp | ||
| // uint64DigestPair is a struct that contains the P-Chain height and the digest | ||
| // of the auxiliary info for which a validator set approval is given. | ||
| type uint64DigestPair [40]byte |
There was a problem hiding this comment.
what if we just had a type approvalKey
type approvalKey struct {
pChainHeight uint64
auxInfoSeqDigest [32]byte
}then we wouldn't need to have the byte logic makeUint64DigestPair and it would work in the map approvalsByPChainHeightAndAuxInfoDigest by default
| return nil, err | ||
| } | ||
| } else { | ||
| // We're not ready for epoch transition yet, so putting a zero-valure approvals here |
There was a problem hiding this comment.
| // We're not ready for epoch transition yet, so putting a zero-valure approvals here | |
| // We're not ready for epoch transition yet, so putting a zero-value approvals here |
| // If the auxiliary info isn't ready for epoch transition, | ||
| // we should focus on contributing to finalizing it before collecting approvals for the epoch transition, | ||
| // as without it being ready, we won't be able to transition epochs anyway. | ||
| auxInf, err := sm.AuxiliaryInfoApp.GenerateAuxInfo(appID, validators, auxInfoHistory) |
There was a problem hiding this comment.
so if we are not done with generating the AuxInfo we will add our own info to hopefully help finish it up. How does this work in the dkg? Does everyone need to contribute to the DKG setup? i assume this cant be the case because then an adversary can just block the epoch from progressing.
There was a problem hiding this comment.
We'll add our own, but it's a bit tricky because the actual implementation of GenerateAuxInfo needs to make sure we don't add our own twice or more.
How does this work in the dkg? Does everyone need to contribute to the DKG setup?
We need enough randomness such that we consider it non-biased and non-colluded.
i assume this cant be the case because then an adversary can just block the epoch from progressing.
Unless we have synchrony assumptions (which we don't)
| } | ||
|
|
||
| // computeAuxInfo computes the AuxiliaryInfo that should be included in the block being built, and whether the auxiliary info history is ready for epoch transition, | ||
| func (sm *StateMachine) computeAuxInfo(parentBlock StateMachineBlock, prevBlockSeq uint64, validators NodeBLSMappings) (*AuxiliaryInfo, bool, [32]byte, error) { |
There was a problem hiding this comment.
should this be simplex.Digest?
| auxInfoDigest = sha256.Sum256(lastHistory(auxInfoHistory)) | ||
| } | ||
|
|
||
| return auxInfo, isAuxInfoReadyForEpochTransition, auxInfoDigest, nil |
There was a problem hiding this comment.
isn't isAuxInfoReadyForEpochTransition implied from auxInfoDigest != empty
There was a problem hiding this comment.
no, auxInfoDigest is just the last one, but it doesn't mean we are ready.
| } else { | ||
| // We're not ready for epoch transition yet, so putting a zero-valure approvals here | ||
| // makes us stay in the collecting approvals state without contributing any approvals. | ||
| newApprovals = &approvals{} |
There was a problem hiding this comment.
one thing i don't get is the aux info is only contributed by the nodes in the current epoch? but for dkg, we want to collect info from nodes in the next epoch? I don't see where we are doing this, as the block builders are only nodes of the current epoch
There was a problem hiding this comment.
Yeah that's a very good question.
So, in the current implementation we cannot do that yet because this will require something similar to what we have with the approvals (an interface that lists approvals that reside in our approval memory pool).
However, when it comes to DKG then if the validator sets of the two adjacent epochs aren't too different, we can use the validator set of the previous epoch.
It's not hard to expand in a later PR to add the Aux Info contribution of nodes of the next epoch, but one thing at a time...
| nextMD := nextBlock.Metadata | ||
| prevMD := parentBlock.Metadata | ||
|
|
||
| auxInfoHistory, auxInfoSeqs, appID, err := collectAuxiliaryInfo(parentBlock, prevBlockSeq, sm.GetBlock, sm.AuxiliaryInfoApp.DefaultAppID()) |
There was a problem hiding this comment.
moreso a general thought, but I feel like this logic is a bit complex to test and reason about in scenarios where we fork due to an empty round timeout.
What happens when we collect all the auxiliary information, but the next block is not built of our previous block and proposes something completely different? do we have tests for scenarios like this?
There was a problem hiding this comment.
moreso a general thought, but I feel like this logic is a bit complex to test and reason about in scenarios where we fork due to an empty round timeout.
What happens when we collect all the auxiliary information, but the next block is not built of our previous block and proposes something completely different? do we have tests for scenarios like this?
Good question.
I did think about this scenario when I implemented it, and I don't think we need to do anything special here.
Essentially, as long as blocks cannot be re-ordered (have their parents changed) after they are built, there is no problem that we can have multiple competing histories.
While a block may not end up being part of a chain, every block that is built on top of a parent block will never have its parent changed, so the checks we have that look at the entire history of auxiliary info are always valid in case the block becomes finalized, and if the block doesn't become finalized and ends up being dropped (rewinding the chain), that's fine.
Not every kind of protocol can support safe rewinding (for example, in a commit-reveal protocol, rewinding is fatal) but the kinds of protocol that we aim to support only involve putting encrypted random shares on-chain, so any item that doesn't end up part of the chain (rewinded) just doesn't end up being part of the DKG, and is anyway comprised of random data, so no information can be learned by an adversary that rewinds the chain.
Introduce an AuxiliaryInfoApp interface abstraction that lets an application
piggyback application-specific data on Simplex epoch changes (e.g.
threshold distributed key generation), and gate the epoch transition on
that data being final. Final doesn't mean finalized as in Simplex,
but rather that the data is "good enough" to be used for the next epoch.
Metadata & encoding:
metadata and an AppID type, with Canoto (de)serialization.
non-empty Info, skipping empty-Info blocks. collectAuxiliaryInfo walks
these pointers to rebuild the epoch's aux info history.
The digest of the last AuxInfo in a final history is signed by the node,
rather than the entire history of non-empty Aux Info.
This is done for flexibility, as some applications like publicly verifiable DKG
have commutative histories.
State machine:
history until IsFinalAuxInfoHistory reports it ready; only then are
next-epoch approvals collected. IsLegalAuxInfoAppend validates a
proposed append on the verify path. Each method also receives the
next epoch's validator set (NodeBLSMappings).
reconstructs the expected AuxiliaryInfo (ApplicationID, PrevAuxInfoSeq)
and enforces it via the block-digest comparison.
Approvals:
(NextPChainReferenceHeight, auxInfoDigest), where auxInfoDigest is the
sha256 of the last aux info element in the history.
so approvals for different digests coexist, and sanitizeApprovals only
aggregates approvals matching the candidate height and digest.
Tests:
(the test signature verifier now actually checks signatures and
(NextPChainReferenceHeight, auxInfoDigest), where auxInfoDigest is the
sha256 of the final aux info history. assembleApprovalToBeSigned
replaces the previous height-only payload.
The switch to ECDSA is because this way we make sure we really sign
the correct digest and not an empty one because of a bug.
so approvals for different digests coexist, and sanitizeApprovals only
aggregates approvals matching the candidate height and digest.