Skip to content

Add handling for Auxiliary info#402

Open
yacovm wants to merge 1 commit into
mainfrom
reconfig-6
Open

Add handling for Auxiliary info#402
yacovm wants to merge 1 commit into
mainfrom
reconfig-6

Conversation

@yacovm

@yacovm yacovm commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

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.

@yacovm yacovm marked this pull request as draft June 9, 2026 21:28
@yacovm yacovm force-pushed the reconfig-6 branch 6 times, most recently from 5fd18af to 98a5139 Compare June 11, 2026 17:25
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>
@yacovm yacovm marked this pull request as ready for review June 11, 2026 17:51
Comment thread msm/msm.go
if len(auxInfo.Info) > 0 {
history = append(history, auxInfo.Info)
seqs = append(seqs, currentSeq)
appID = auxInfo.ApplicationID

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment thread msm/msm.go
Comment on lines +1113 to +1116
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah we can do that. Initially I only had the history and then added the seqs. Will change

Comment thread msm/msm.go

// 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).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i think the applicationID returned is actually the oldest

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah, you're right and that's by design. Will fix

Comment thread msm/approvals.go
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

makes sense

Comment thread msm/msm.go
return nil, err
}
} else {
// We're not ready for epoch transition yet, so putting a zero-valure approvals here

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// 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

Comment thread msm/msm.go
// 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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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)

Comment thread msm/msm.go
}

// 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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should this be simplex.Digest?

Comment thread msm/msm.go
auxInfoDigest = sha256.Sum256(lastHistory(auxInfoHistory))
}

return auxInfo, isAuxInfoReadyForEpochTransition, auxInfoDigest, nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

isn't isAuxInfoReadyForEpochTransition implied from auxInfoDigest != empty

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

no, auxInfoDigest is just the last one, but it doesn't mean we are ready.

Comment thread msm/msm.go
} 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{}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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...

Comment thread msm/msm.go
nextMD := nextBlock.Metadata
prevMD := parentBlock.Metadata

auxInfoHistory, auxInfoSeqs, appID, err := collectAuxiliaryInfo(parentBlock, prevBlockSeq, sm.GetBlock, sm.AuxiliaryInfoApp.DefaultAppID())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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?

@yacovm yacovm Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants