-
Notifications
You must be signed in to change notification settings - Fork 4
Implement approval store #384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||
| // See the file LICENSE for licensing terms. | ||
|
|
||
| package metadata | ||
|
|
||
| import ( | ||
| "encoding/asn1" | ||
| "encoding/binary" | ||
| "fmt" | ||
|
|
||
| "github.com/ava-labs/simplex" | ||
| "go.uber.org/zap" | ||
| ) | ||
|
|
||
| type approvalsByPChainHeight map[uint64]*approvalAndTimestamp | ||
|
|
||
| type approvalAndTimestamp struct { | ||
| ValidatorSetApproval | ||
| Timestamp uint64 | ||
| } | ||
|
|
||
| type ApprovalStore struct { | ||
| signatureVerifier SignatureVerifier | ||
| validators NodeBLSMappings | ||
| logger simplex.Logger | ||
| pkByNodeID map[nodeID][]byte | ||
| approvalsByNodes map[nodeID]approvalsByPChainHeight | ||
| storedCount int | ||
| } | ||
|
|
||
| func NewApprovalStore(signatureVerifier SignatureVerifier, validators NodeBLSMappings, logger simplex.Logger) *ApprovalStore { | ||
| pkByNodeID := make(map[nodeID][]byte) | ||
| for _, vdr := range validators { | ||
| pkByNodeID[vdr.NodeID] = vdr.BLSKey | ||
| } | ||
|
|
||
| approvalsByNodes := make(map[nodeID]approvalsByPChainHeight, len(validators)) | ||
| for _, vdr := range validators { | ||
| approvalsByNodes[vdr.NodeID] = make(approvalsByPChainHeight) | ||
| } | ||
|
|
||
| return &ApprovalStore{ | ||
| signatureVerifier: signatureVerifier, | ||
| validators: validators, | ||
| pkByNodeID: pkByNodeID, | ||
| logger: logger, | ||
| approvalsByNodes: approvalsByNodes, | ||
| } | ||
| } | ||
|
|
||
| func (as *ApprovalStore) Approvals() ValidatorSetApprovals { | ||
| approvals := make(ValidatorSetApprovals, 0, as.storedCount) | ||
| for _, approvalsByHeight := range as.approvalsByNodes { | ||
| for _, approval := range approvalsByHeight { | ||
| approvals = append(approvals, (*approval).ValidatorSetApproval) | ||
| } | ||
| } | ||
| return approvals | ||
| } | ||
|
|
||
| func (as *ApprovalStore) HandleApproval(approval *ValidatorSetApproval, timestamp uint64) error { | ||
| // First thing we check is if the node that sent this approval is a validator. | ||
| pk, exists := as.getPKOfNode(approval.NodeID) | ||
| if !exists { | ||
| as.logger.Debug("Received an approval from a node that is not a validator", zap.String("nodeID", | ||
| fmt.Sprintf("%x", approval.NodeID)), zap.Uint64("pChainHeight", approval.PChainHeight)) | ||
| return nil | ||
| } | ||
|
|
||
| // Second thing we check is if we already have an approval for this height from this node. | ||
| if as.approvalExistsAndUpToDate(approval, timestamp) { | ||
| as.logger.Debug("Already have an approval from the node", zap.String("nodeID", | ||
| fmt.Sprintf("%x", approval.NodeID)), zap.Uint64("pChainHeight", approval.PChainHeight)) | ||
| return nil | ||
| } | ||
|
|
||
| // Third thing we check is if the signature of the approval is valid. | ||
| // We need it to be valid in order for nodes to be able to aggregate it later on along with other approvals. | ||
| if err := as.checkApprovalSignature(approval, pk); err != nil { | ||
| as.logger.Debug("Received an approval with an invalid signature", zap.String("nodeID", | ||
| fmt.Sprintf("%x", approval.NodeID)), zap.Uint64("pChainHeight", approval.PChainHeight)) | ||
| return nil | ||
| } | ||
|
|
||
| // Store the approval. | ||
| oldApproval := as.approvalsByNodes[approval.NodeID][approval.PChainHeight] | ||
| as.approvalsByNodes[approval.NodeID][approval.PChainHeight] = &approvalAndTimestamp{ | ||
| ValidatorSetApproval: *approval, | ||
| Timestamp: timestamp, | ||
| } | ||
|
|
||
| if oldApproval == nil { | ||
| as.storedCount++ | ||
| } | ||
|
|
||
| // We only store the last |as.validators| of approvals for each node, | ||
| // so we need to delete old approvals if we have more than |as.validators| approvals stored for this node. | ||
| as.maybePruneOldApprovals(approval) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (as *ApprovalStore) maybePruneOldApprovals(approval *ValidatorSetApproval) { | ||
| if len(as.approvalsByNodes[approval.NodeID]) <= len(as.validators) { | ||
| return | ||
| } | ||
| // Find the oldest approval and delete it. | ||
| var oldestApproval *approvalAndTimestamp | ||
| for _, approval := range as.approvalsByNodes[approval.NodeID] { | ||
| if oldestApproval == nil || approval.Timestamp < oldestApproval.Timestamp { | ||
| oldestApproval = approval | ||
| } | ||
| } | ||
|
|
||
| if oldestApproval != nil { | ||
| as.logger.Debug("Deleting old approval from node", | ||
| zap.String("nodeID", fmt.Sprintf("%x", oldestApproval.NodeID)), | ||
| zap.String("oldestApprovalPChainHeight", | ||
| fmt.Sprintf("%d", oldestApproval.PChainHeight)), zap.Uint64("oldestApprovalTimestamp", oldestApproval.Timestamp)) | ||
| delete(as.approvalsByNodes[approval.NodeID], oldestApproval.PChainHeight) | ||
| as.storedCount-- | ||
| } | ||
| } | ||
|
|
||
| func (as *ApprovalStore) checkApprovalSignature(approval *ValidatorSetApproval, pk []byte) error { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm is it not a problem that we verify the signature is valid over just the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, because when I implemented this, I still haven't done that domain separation in the MSM. Will fix 👍
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it still not a problem though that we don't check the approval signature against the timestamp? we also don't pass in who sent the approval so someone could send an approval with a timestamp = MaxUint64 and say it was from another node. This would remove all other approvals from that node in
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you have answered your own question. This only works if the dissemination of the approvals is point to point, over authenticated channels, and not via gossip.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also recall that: This doesn't include timestamps, and we naturally cannot expect all signatures to match a single timestamp. The timestamp is just there to give us liveness when a node crashes and restarts or picks a different P-chain height to sign over. We need some way of disposing of "old" in-memory values. We could have the timestamp be even computed locally inside of
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
might be a nice change, why send it if we don't need to.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| pChainHeight := approval.PChainHeight | ||
| pChainHeightBuff := make([]byte, 8) | ||
| binary.BigEndian.PutUint64(pChainHeightBuff, pChainHeight) | ||
|
|
||
| signedMsg := simplex.SignedMessage{Payload: pChainHeightBuff, Context: signatureContext} | ||
| toBeSigned, err := asn1.Marshal(signedMsg) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // We check if the signature is valid before we store the approval. | ||
| return as.signatureVerifier.VerifySignature(approval.Signature, toBeSigned, pk) | ||
| } | ||
|
|
||
| func (as *ApprovalStore) getPKOfNode(nodeID nodeID) ([]byte, bool) { | ||
| pk, exists := as.pkByNodeID[nodeID] | ||
| return pk, exists | ||
| } | ||
|
|
||
| func (as *ApprovalStore) approvalExistsAndUpToDate(approval *ValidatorSetApproval, timestamp uint64) bool { | ||
| if as.approvalsByNodes[approval.NodeID] == nil { | ||
| return false | ||
| } | ||
| existingApproval := as.approvalsByNodes[approval.NodeID][approval.PChainHeight] | ||
| if existingApproval == nil { | ||
| return false | ||
| } | ||
|
|
||
| return existingApproval.Timestamp >= timestamp | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.