Skip to content
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

Add node ID to index mapping to DKG data model #441

Open
wants to merge 36 commits into
base: feature/efm-recovery
Choose a base branch
from

Conversation

jordanschalm
Copy link
Member

@jordanschalm jordanschalm commented Aug 7, 2024

Ref: onflow/flow-go#6213

General Context

This PR modifies the DKG result data model to include an explicit mapping from node ID to DKG index. Currently the submission is an ordered list of keys, with the mapping being implicit based on the ordering. To support EFM Recovery we need to explicitly encode the index mapping and include it in the EpochCommit event. See onflow/flow-go#6213 for details.

📝 Notes about implementation options from original draft

I broadly see two approaches to implement this:

  1. Add contract fields (storage slots, because of upgrade limitations) which includes only the new index mapping, conceptually extending the existing fields finalSubmissionByNodeID and uniqueFinalSubmissions.
    The contract would use the new and existing fields together, and would need to maintain consistency between them.
  2. Add one contract field which tracks the entire submission structure (now including both a key list and index mapping), conceptually replacing the existing fields finalSubmissionByNodeID and uniqueFinalSubmissions.
    The existing fields would be deprecated (we can't remove them because of upgrade limitations, but they would not be referenced by any code in the contract).

After noodling around a bit with both, I'm inclined to go with Option 2, because it seems easier to work with and reason about the submission state when it's encapsulated in one field (and type).

Changes

  • Add type ResultSubmission that encapsulates one complete DKG submission, (replaces [String?])
  • Add type SubmissionTracker that encapsulates the state for all DKG submissions for one DKG instance
  • Deprecate existing contract fields which track final submissions as [String]
  • Adds dkgGroupKey and dkgIdMapping fields to EpochCommit (previously the group key was by convention the first entry in the single dkgPubKeys entry

@jordanschalm jordanschalm changed the base branch from master to feature/efm-recovery August 7, 2024 16:49
@joshuahannan
Copy link
Member

I'm also leaning towards option 2. We actually can remove fields in upgrades. It doesn't delete the state, but it removes the ability to access it anywhere, so that is an option. Might be good to keep the field though for the small chance that we might actually need it again in the future

Copy link
Member

@joshuahannan joshuahannan left a comment

Choose a reason for hiding this comment

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

This looks like a good start!

return false
}
// TODO: validate equality check on list is safe
if self.pubKeys != other.pubKeys {
Copy link
Member

Choose a reason for hiding this comment

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

Do we expect these to be in the same exact order or just contain the same keys?

Copy link
Member Author

Choose a reason for hiding this comment

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

Equivalent ResultSubmission would need to have them in the same order, because the index is meaningful in the DKG.

@@ -357,6 +426,7 @@ access(all) contract FlowDKG {
}

/// Get the count of the final submissions array
// TODO: Should this be a view function?
Copy link
Member

Choose a reason for hiding this comment

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

yes, probably should be view

@jordanschalm
Copy link
Member Author

I'm also leaning towards option 2. We actually can remove fields in upgrades. It doesn't delete the state, but it removes the ability to access it anywhere, so that is an option. Might be good to keep the field though for the small chance that we might actually need it again in the future

Sounds good. We'll move ahead in that direction then - thanks for the review! I agree keeping the deprecated fields (at least at first) is the better move.

@jordanschalm jordanschalm marked this pull request as ready for review September 17, 2024 18:26
Copy link
Member Author

@jordanschalm jordanschalm left a comment

Choose a reason for hiding this comment

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

Adding some notes for reviewers

// By convention, all keys are encoded as lowercase hex strings, though it is not enforced here.
//
// INVARIANTS:
// (1) either all fields are nil (empty submission) or no fields are nil
Copy link
Member Author

Choose a reason for hiding this comment

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

Previously, the data model for submissions was [String?], so some keys could be nil and some could be non-nil. If any keys were nil, we considered the submission invalid ("empty" in the terminology used in this PR).

This PR changes the data model so that either all fields are nil, or none are, and enforces this in the constructor.

@@ -119,6 +333,7 @@ access(all) contract FlowDKG {

/// Posts a whiteboard message to the contract
access(all) fun postMessage(_ content: String) {
// TODO: DKG enabled?
Copy link
Member Author

Choose a reason for hiding this comment

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

It seems like the smart contract allows posting whiteboard messages and sending final submissions even when the DKG is disabled 🤔. I don't think this matters functionally because:

  • the DKG cannot be ended (winning final submission retrieved) if it is disabled
  • when enabled/started, all state including whiteboard messages and submissions are wiped

For clarity though, I think we should disallow interacting with the contract while it is disabled.

Copy link
Member

Choose a reason for hiding this comment

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

That is fine with me. Feel free to add those pre-conditions to the functions if you want!

@@ -279,36 +442,9 @@ access(all) contract FlowDKG {
}
}

/// Checks if two final submissions are equal by comparing each element
/// Each element has to be exactly the same and in the same order
access(account) fun submissionsEqual(_ existingSubmission: [String?], _ submission: [String?]): Bool {
Copy link
Member Author

Choose a reason for hiding this comment

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

Replaced with ResultSubmission.equals

@@ -139,57 +354,9 @@ access(all) contract FlowDKG {
/// Sends the final key vector submission.
/// Can only be called by consensus nodes that are registered
/// and can only be called once per consensus node per epoch
access(all) fun sendFinalSubmission(_ submission: [String?]) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Replaced with SubmissionTracker methods

}

// Borrows the singleton SubmissionTracker from storage; panics if none exists.
access(contract) view fun mustBorrowSubmissionTracker(): &FlowDKG.SubmissionTracker {
Copy link
Member Author

Choose a reason for hiding this comment

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

This can be used to read from SubmissionTracker in a view-only context.

@@ -19,10 +19,6 @@ import (
"github.com/onflow/flow-core-contracts/lib/go/templates"
Copy link
Member Author

Choose a reason for hiding this comment

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

These tests are updated to the point where they are passing, however there are still several places (noted as TODO comments) where additional updates should be made prior to merging. In particular cases where the test is expecting a failure, but the failure occurs for the wrong reason (like passing in the old transaction arguments).

@jordanschalm jordanschalm changed the title [DRAFT] Add node ID to index mapping to DKG data model Add node ID to index mapping to DKG data model Sep 17, 2024
Copy link
Member

@joshuahannan joshuahannan left a comment

Choose a reason for hiding this comment

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

Looks pretty good! I just had a few comments. I appreciate converting some of the tests to the Cadence testing framework. Still will need to make sure all the important transactions are tested in Go though because we need to make sure the templates package is still working properly and returning valid transactions.

Comment on lines +98 to +102
if groupPubKey == nil && pubKeys == nil && idMapping == nil {
return true
}
// Otherwise, all fields must be non-nil
return groupPubKey != nil && pubKeys != nil && idMapping != nil
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little confused. The comments make it seem like this function is checking if any submission is valid, but the function name is isValidNilSubmission, which makes me think that this should only verify that the all-nil case is valid. Maybe it should be renamed to isValidSubmission? Also, we can simplify the conditional I think:

        return (groupPubKey == nil && pubKeys == nil && idMapping == nil)
        || (groupPubKey != nil && pubKeys != nil && idMapping != nil)


// Checks that an id mapping (part of ResultSubmission) contains one entry per public key.
access(all) view fun isValidIDMapping(pubKeys: [String]?, idMapping: {String: Int}?): Bool {
if pubKeys == nil {
Copy link
Member

Choose a reason for hiding this comment

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

Should this be pubKeys == nil && idMapping == nil?


init(groupPubKey: String?, pubKeys: [String]?, idMapping: {String:Int}?) {
pre {
FlowDKG.isValidNilSubmission(groupPubKey: groupPubKey, pubKeys: pubKeys, idMapping: idMapping): "invalid empty submission"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
FlowDKG.isValidNilSubmission(groupPubKey: groupPubKey, pubKeys: pubKeys, idMapping: idMapping): "invalid empty submission"
FlowDKG.isValidNilSubmission(groupPubKey: groupPubKey, pubKeys: pubKeys, idMapping: idMapping):
"FlowDKG.ResultSubmission.init: Invalid DKG Result Submission! "
.concat("DKG fields must all be `nil` or all be non-`nil`")

I'm going through all the core contracts repos and updating the error messages to be more helpful, so it would be good to start doing this for all our PRs from now on. Basically going off the guidelines in this issue. Can you do something similar for the other panics in this PR?

// CAUTION: This method should only be called by Participant, which enforces that Participant.nodeID is passed in.
access(all) fun addSubmission(nodeID: String, submission: ResultSubmission) {
pre {
self.authorized[nodeID] != nil: "must be authorized for this DKG instance"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
self.authorized[nodeID] != nil: "must be authorized for this DKG instance"
self.authorized[nodeID] != nil:
"FlowDKG.addSubmission: Cannot add Result Submission for DKG for epoch "
.concat(FlowEpoch.proposedEpochCounter().toString())
.concat(". The node with id ").concat(nodeID)
.concat(" that is submitting the Result Submission is not authorized for this DKG instance."

Same comment about error messages here.

@@ -119,6 +333,7 @@ access(all) contract FlowDKG {

/// Posts a whiteboard message to the contract
access(all) fun postMessage(_ content: String) {
// TODO: DKG enabled?
Copy link
Member

Choose a reason for hiding this comment

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

That is fine with me. Feel free to add those pre-conditions to the functions if you want!


// Borrows the singleton SubmissionTracker from storage; panics if none exists.
access(contract) view fun mustBorrowSubmissionTracker(): &FlowDKG.SubmissionTracker {
return self.account.storage.borrow<&SubmissionTracker>(from: /storage/flowDKGFinalSubmissionTracker)!
Copy link
Member

Choose a reason for hiding this comment

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

Should we add a ?? panic() here?


prepare(signer: auth(BorrowValue) &Account) {
self.dkgParticipant = signer.storage.borrow<&FlowDKG.Participant>(from: FlowDKG.ParticipantStoragePath)
?? panic("Cannot borrow dkg participant reference")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
?? panic("Cannot borrow dkg participant reference")
?? panic("Cannot borrow DKG Participant Reference from path "
.concat(FlowDKG.ParticipantStoragePath)
.concat(". The signer needs to make sure their account is initialized with the DKG Participant resource."))

It would be good to start updating the error messages here too. This would apply to most of the DKG transactions here too

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