diff --git a/engine/execution/state/bootstrap/bootstrap_test.go b/engine/execution/state/bootstrap/bootstrap_test.go index 03e8357489e..25a207581f8 100644 --- a/engine/execution/state/bootstrap/bootstrap_test.go +++ b/engine/execution/state/bootstrap/bootstrap_test.go @@ -53,7 +53,7 @@ func TestBootstrapLedger(t *testing.T) { } func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) { - expectedStateCommitmentBytes, _ := hex.DecodeString("5cb14268cf6f7aa5ad4487e47aaae8d9b4c32b30c98373d35670d0c7bc080153") + expectedStateCommitmentBytes, _ := hex.DecodeString("95b38890e552404e2cfbd95796a812cc66e4ca273d869c2b82079dda60d6a65e") expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes) require.NoError(t, err) diff --git a/go.mod b/go.mod index 68d5f38cdd6..f818311baf9 100644 --- a/go.mod +++ b/go.mod @@ -51,8 +51,8 @@ require ( github.com/onflow/cadence v1.0.0-preview.52 github.com/onflow/crypto v0.25.2 github.com/onflow/flow v0.3.4 - github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4 - github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4 + github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af + github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af github.com/onflow/flow-go-sdk v1.0.0-preview.55 github.com/onflow/flow/protobuf/go/flow v0.4.7 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 diff --git a/go.sum b/go.sum index b52be44bb04..f0fb26b248a 100644 --- a/go.sum +++ b/go.sum @@ -2178,10 +2178,10 @@ github.com/onflow/crypto v0.25.2 h1:GjHunqVt+vPcdqhxxhAXiMIF3YiLX7gTuTR5O+VG2ns= github.com/onflow/crypto v0.25.2/go.mod h1:fY7eLqUdMKV8EGOw301unP8h7PvLVy8/6gVR++/g0BY= github.com/onflow/flow v0.3.4 h1:FXUWVdYB90f/rjNcY0Owo30gL790tiYff9Pb/sycXYE= github.com/onflow/flow v0.3.4/go.mod h1:lzyAYmbu1HfkZ9cfnL5/sjrrsnJiUU8fRL26CqLP7+c= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4 h1:pO1N4b/v2t/yRmeSfVkjYfrDDtFwvjsHgVgN+FRDQsM= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= -github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4 h1:Fr1QYriOtBDgH1BLYKp1rPONndZ5qz8LsNK98LAT+YI= -github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af h1:1vg6OyFMigNoyqk4SWaMlfIr5POiFX9SpFpcCvrKrUc= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af h1:Vzaw1OSMOKnS3zGVyv0kgDRX+Owsj/IF4B0vbEO7kOk= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af/go.mod h1:l8eCazFlUva+sQzh5hBWfOtk+iLmVYJ2DuhR2jSP06o= github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/Sy/tIXdw90yKHcV0= github.com/onflow/flow-ft/lib/go/contracts v1.0.0/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.0 h1:6cMS/lUJJ17HjKBfMO/eh0GGvnpElPgBXx7h5aoWJhs= diff --git a/insecure/go.mod b/insecure/go.mod index 83c6874e1fd..47eff0ea9d8 100644 --- a/insecure/go.mod +++ b/insecure/go.mod @@ -204,8 +204,8 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onflow/atree v0.8.0-rc.6 // indirect github.com/onflow/cadence v1.0.0-preview.52 // indirect - github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4 // indirect - github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4 // indirect + github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af // indirect + github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af // indirect github.com/onflow/flow-ft/lib/go/contracts v1.0.0 // indirect github.com/onflow/flow-ft/lib/go/templates v1.0.0 // indirect github.com/onflow/flow-go-sdk v1.0.0-preview.55 // indirect diff --git a/insecure/go.sum b/insecure/go.sum index 65115363658..b431e41de15 100644 --- a/insecure/go.sum +++ b/insecure/go.sum @@ -2164,10 +2164,10 @@ github.com/onflow/cadence v1.0.0-preview.52/go.mod h1:7wvvecnAZtYOspLOS3Lh+FuAmM github.com/onflow/crypto v0.25.0/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= github.com/onflow/crypto v0.25.2 h1:GjHunqVt+vPcdqhxxhAXiMIF3YiLX7gTuTR5O+VG2ns= github.com/onflow/crypto v0.25.2/go.mod h1:fY7eLqUdMKV8EGOw301unP8h7PvLVy8/6gVR++/g0BY= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4 h1:pO1N4b/v2t/yRmeSfVkjYfrDDtFwvjsHgVgN+FRDQsM= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= -github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4 h1:Fr1QYriOtBDgH1BLYKp1rPONndZ5qz8LsNK98LAT+YI= -github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af h1:1vg6OyFMigNoyqk4SWaMlfIr5POiFX9SpFpcCvrKrUc= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af h1:Vzaw1OSMOKnS3zGVyv0kgDRX+Owsj/IF4B0vbEO7kOk= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af/go.mod h1:l8eCazFlUva+sQzh5hBWfOtk+iLmVYJ2DuhR2jSP06o= github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/Sy/tIXdw90yKHcV0= github.com/onflow/flow-ft/lib/go/contracts v1.0.0/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A= github.com/onflow/flow-ft/lib/go/templates v1.0.0 h1:6cMS/lUJJ17HjKBfMO/eh0GGvnpElPgBXx7h5aoWJhs= diff --git a/integration/dkg/dkg_client_test.go b/integration/dkg/dkg_client_test.go index 2399c1401e5..ca38d7dcbc3 100644 --- a/integration/dkg/dkg_client_test.go +++ b/integration/dkg/dkg_client_test.go @@ -149,19 +149,9 @@ func (s *ClientSuite) TestNilDKGSubmission() { // prepare DKG clients := s.prepareDKG(participants) - // generate list of public keys - numberOfNodes := len(participants) - publicKeys := make([]crypto.PublicKey, 0, numberOfNodes+1) - for i := 0; i < numberOfNodes; i++ { - publicKeys = append(publicKeys, nil) - } - - // create a nil group public key - var groupPublicKey crypto.PublicKey - // submit empty nil keys for each participant for _, client := range clients { - err := client.SubmitResult(groupPublicKey, publicKeys) + err := client.SubmitEmptyResult() require.NoError(s.T(), err) } } @@ -178,14 +168,16 @@ func (s *ClientSuite) TestSubmitResult() { // generate list of public keys numberOfNodes := len(participants) publicKeys := make([]crypto.PublicKey, 0, numberOfNodes) + indexMap := make(flow.DKGIndexMap, numberOfNodes) for i := 0; i < numberOfNodes; i++ { privateKey := unittest.KeyFixture(crypto.BLSBLS12381) publicKeys = append(publicKeys, privateKey.PublicKey()) + indexMap[participants[i]] = i } // create a group public key groupPublicKey := unittest.KeyFixture(crypto.BLSBLS12381).PublicKey() - err := clients[0].SubmitResult(groupPublicKey, publicKeys) + err := clients[0].SubmitParametersAndResult(indexMap, groupPublicKey, publicKeys) require.NoError(s.T(), err) } @@ -232,7 +224,7 @@ func (s *ClientSuite) setUpAdmin() { // set up admin resource setUpAdminTx := sdk.NewTransaction(). - SetScript(templates.GeneratePublishDKGParticipantScript(s.env)). + SetScript(templates.GeneratePublishDKGAdminScript(s.env)). SetComputeLimit(9999). SetProposalKey(s.blockchain.ServiceKey().Address, s.blockchain.ServiceKey().Index, s.blockchain.ServiceKey().SequenceNumber). diff --git a/integration/dkg/dkg_client_wrapper.go b/integration/dkg/dkg_client_wrapper.go index 9d9b4e5ca5f..e46a5fb52c8 100644 --- a/integration/dkg/dkg_client_wrapper.go +++ b/integration/dkg/dkg_client_wrapper.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + "github.com/onflow/flow-go/module" + "github.com/onflow/crypto" "go.uber.org/atomic" @@ -27,6 +29,8 @@ type DKGClientWrapper struct { enabled *atomic.Bool } +var _ module.DKGContractClient = (*DKGClientWrapper)(nil) + // NewDKGClientWrapper instantiates a new DKGClientWrapper func NewDKGClientWrapper(client *dkgmod.Client) *DKGClientWrapper { return &DKGClientWrapper{ @@ -76,10 +80,18 @@ func (c *DKGClientWrapper) ReadBroadcast(fromIndex uint, referenceBlock flow.Ide return c.client.ReadBroadcast(fromIndex, referenceBlock) } -// SubmitResult implements the DKGContractClient interface -func (c *DKGClientWrapper) SubmitResult(groupPubKey crypto.PublicKey, pubKeys []crypto.PublicKey) error { +// SubmitParametersAndResult implements the DKGContractClient interface +func (c *DKGClientWrapper) SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupPubKey crypto.PublicKey, pubKeys []crypto.PublicKey) error { if !c.enabled.Load() { return fmt.Errorf("failed to submit DKG result: %w", errClientDisabled) } - return c.client.SubmitResult(groupPubKey, pubKeys) + return c.client.SubmitParametersAndResult(indexMap, groupPubKey, pubKeys) +} + +// SubmitEmptyResult implements the DKGContractClient interface +func (c *DKGClientWrapper) SubmitEmptyResult() error { + if !c.enabled.Load() { + return fmt.Errorf("failed to submit empty DKG result: %w", errClientDisabled) + } + return c.client.SubmitEmptyResult() } diff --git a/integration/dkg/dkg_emulator_suite.go b/integration/dkg/dkg_emulator_suite.go index ee66e7e594c..6d1677029e3 100644 --- a/integration/dkg/dkg_emulator_suite.go +++ b/integration/dkg/dkg_emulator_suite.go @@ -2,9 +2,13 @@ package dkg import ( "context" + "encoding/hex" "fmt" "os" + "github.com/onflow/crypto" + "golang.org/x/exp/slices" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -66,6 +70,9 @@ func (s *EmulatorSuite) SetupTest() { s.setupDKGAdmin() boostrapNodesInfo := unittest.PrivateNodeInfosFixture(numberOfNodes, unittest.WithRole(flow.RoleConsensus)) + slices.SortFunc(boostrapNodesInfo, func(lhs, rhs bootstrap.NodeInfo) int { + return flow.IdentifierCanonical(lhs.NodeID, rhs.NodeID) + }) for _, id := range boostrapNodesInfo { s.nodeAccounts = append(s.nodeAccounts, s.createAndFundAccount(id)) s.netIDs = append(s.netIDs, id.Identity()) @@ -160,7 +167,7 @@ func (s *EmulatorSuite) deployDKGContract() { func (s *EmulatorSuite) setupDKGAdmin() { setUpAdminTx := sdk.NewTransaction(). - SetScript(templates.GeneratePublishDKGParticipantScript(s.env)). + SetScript(templates.GeneratePublishDKGAdminScript(s.env)). SetComputeLimit(9999). SetProposalKey( s.blockchain.ServiceKey().Address, @@ -397,31 +404,38 @@ func (s *EmulatorSuite) isDKGCompleted() bool { return bool(value.(cadence.Bool)) } -func (s *EmulatorSuite) getResult() []string { - script := fmt.Sprintf(` - import FlowDKG from 0x%s - - access(all) fun main(): [String?]? { - return FlowDKG.dkgCompleted() - } `, - s.env.DkgAddress, - ) - - res := s.executeScript([]byte(script), nil) +// getParametersAndResult retrieves the DKG setup parameters (`flow.DKGIndexMap`) and the DKG result from the DKG white-board smart contract. +func (s *EmulatorSuite) getParametersAndResult() (flow.DKGIndexMap, crypto.PublicKey, []crypto.PublicKey) { + res := s.executeScript(templates.GenerateGetDKGCanonicalFinalSubmissionScript(s.env), nil) value := res.(cadence.Optional).Value if value == nil { - return []string{} + s.Fail("DKG result is nil") } - dkgResult := []string{} - for _, item := range value.(cadence.Array).Values { - dkgResult = append( - dkgResult, - string(item.(cadence.Optional).Value.(cadence.String)), - ) + decodePubkey := func(r string) crypto.PublicKey { + pkBytes, err := hex.DecodeString(r) + require.NoError(s.T(), err) + pk, err := crypto.DecodePublicKey(crypto.BLSBLS12381, pkBytes) + require.NoError(s.T(), err) + return pk + } + + fields := value.(cadence.Struct).FieldsMappedByName() + groupKey := decodePubkey(string(UnwrapOptional[cadence.String](fields["groupPubKey"]))) + + dkgKeyShares := CadenceArrayTo(UnwrapOptional[cadence.Array](fields["pubKeys"]), func(value cadence.Value) crypto.PublicKey { + return decodePubkey(string(value.(cadence.String))) + }) + + cdcIndexMap := CDCToDKGIDMapping(UnwrapOptional[cadence.Dictionary](fields["idMapping"])) + indexMap := make(flow.DKGIndexMap, len(cdcIndexMap)) + for k, v := range cdcIndexMap { + nodeID, err := flow.HexStringToIdentifier(k) + require.NoError(s.T(), err) + indexMap[nodeID] = v } - return dkgResult + return indexMap, groupKey, dkgKeyShares } func (s *EmulatorSuite) initEngines(node *node, ids flow.IdentityList) { @@ -529,3 +543,26 @@ func (s *EmulatorSuite) executeScript(script []byte, arguments [][]byte) cadence require.True(s.T(), result.Succeeded()) return result.Value } + +func UnwrapOptional[T cadence.Value](optional cadence.Value) T { + return optional.(cadence.Optional).Value.(T) +} + +func CadenceArrayTo[T any](arr cadence.Value, convert func(cadence.Value) T) []T { + out := make([]T, len(arr.(cadence.Array).Values)) + for i := range out { + out[i] = convert(arr.(cadence.Array).Values[i]) + } + return out +} + +func CDCToDKGIDMapping(cdc cadence.Value) map[string]int { + idMappingCDC := cdc.(cadence.Dictionary) + idMapping := make(map[string]int, len(idMappingCDC.Pairs)) + for _, pair := range idMappingCDC.Pairs { + nodeID := string(pair.Key.(cadence.String)) + index := pair.Value.(cadence.Int).Int() + idMapping[nodeID] = index + } + return idMapping +} diff --git a/integration/dkg/dkg_emulator_test.go b/integration/dkg/dkg_emulator_test.go index 2243e44f9cc..4e61ee37127 100644 --- a/integration/dkg/dkg_emulator_test.go +++ b/integration/dkg/dkg_emulator_test.go @@ -1,7 +1,6 @@ package dkg import ( - "encoding/hex" "fmt" "math/rand" "testing" @@ -128,22 +127,7 @@ func (s *EmulatorSuite) runTest(goodNodes int, emulatorProblems bool) { // the result is an array of public keys where the first item is the group // public key - res := s.getResult() - - assert.Equal(s.T(), len(s.nodes)+1, len(res)) - pubKeys := make([]crypto.PublicKey, 0, len(res)) - for _, r := range res { - pkBytes, err := hex.DecodeString(r) - assert.NoError(s.T(), err) - pk, err := crypto.DecodePublicKey(crypto.BLSBLS12381, pkBytes) - assert.NoError(s.T(), err) - pubKeys = append(pubKeys, pk) - } - - groupPubKeyBytes, err := hex.DecodeString(res[0]) - assert.NoError(s.T(), err) - groupPubKey, err := crypto.DecodePublicKey(crypto.BLSBLS12381, groupPubKeyBytes) - assert.NoError(s.T(), err) + _, groupPubKey, pubKeys := s.getParametersAndResult() tag := "some tag" hasher := msig.NewBLSHasher(tag) @@ -162,9 +146,9 @@ func (s *EmulatorSuite) runTest(goodNodes int, emulatorProblems bool) { signatures = append(signatures, signature) indices = append(indices, i) - ok, err := pubKeys[i+1].Verify(signature, sigData, hasher) + ok, err := pubKeys[i].Verify(signature, sigData, hasher) require.NoError(s.T(), err) - assert.True(s.T(), ok, fmt.Sprintf("signature %d share doesn't verify under the public key share", i+1)) + assert.True(s.T(), ok, fmt.Sprintf("signature %d share doesn't verify under the public key share", i)) } // shuffle the signatures and indices before constructing the group diff --git a/integration/dkg/dkg_whiteboard_client.go b/integration/dkg/dkg_whiteboard_client.go index 9b4593cefae..9928f1bfaf5 100644 --- a/integration/dkg/dkg_whiteboard_client.go +++ b/integration/dkg/dkg_whiteboard_client.go @@ -3,6 +3,8 @@ package dkg import ( "sync" + "github.com/onflow/flow-go/module" + "github.com/onflow/crypto" "github.com/onflow/flow-go/model/flow" @@ -17,6 +19,8 @@ type WhiteboardClient struct { whiteboard *whiteboard } +var _ module.DKGContractClient = (*WhiteboardClient)(nil) + // NewWhiteboardClient instantiates a new WhiteboardClient with a reference to // an existing whiteboard object. func NewWhiteboardClient(nodeID flow.Identifier, whiteboard *whiteboard) *WhiteboardClient { @@ -44,10 +48,17 @@ func (wc *WhiteboardClient) ReadBroadcast(fromIndex uint, referenceBlock flow.Id return msgs, nil } -// SubmitResult implements the DKGContractClient interface. It publishes the +// SubmitParametersAndResult implements the DKGContractClient interface. It publishes the // DKG results under the node's ID. -func (wc *WhiteboardClient) SubmitResult(groupKey crypto.PublicKey, pubKeys []crypto.PublicKey) error { - wc.whiteboard.submit(wc.nodeID, groupKey, pubKeys) +func (wc *WhiteboardClient) SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupKey crypto.PublicKey, pubKeys []crypto.PublicKey) error { + wc.whiteboard.submit(wc.nodeID, groupKey, pubKeys, indexMap) + return nil +} + +// SubmitEmptyResult implements the DKGContractClient interface. It publishes the +// empty DKG result under the node's ID. +func (wc *WhiteboardClient) SubmitEmptyResult() error { + wc.whiteboard.submit(wc.nodeID, nil, nil, nil) return nil } @@ -68,6 +79,7 @@ type whiteboard struct { type result struct { groupKey crypto.PublicKey pubKeys []crypto.PublicKey + indexMap flow.DKGIndexMap } // Fingerprint implements the Fingerprinter interface used by MakeID @@ -99,11 +111,16 @@ func (w *whiteboard) read(fromIndex uint) []messages.BroadcastDKGMessage { return w.messages[fromIndex:] } -func (w *whiteboard) submit(nodeID flow.Identifier, groupKey crypto.PublicKey, pubKeys []crypto.PublicKey) { +func (w *whiteboard) submit( + nodeID flow.Identifier, + groupKey crypto.PublicKey, + pubKeys []crypto.PublicKey, + indexMap flow.DKGIndexMap, +) { w.Lock() defer w.Unlock() - result := result{groupKey: groupKey, pubKeys: pubKeys} + result := result{groupKey: groupKey, pubKeys: pubKeys, indexMap: indexMap} resultHash := flow.MakeID(result) w.results[resultHash] = result diff --git a/integration/dkg/dkg_whiteboard_test.go b/integration/dkg/dkg_whiteboard_test.go index 58119f7027a..205dd454adb 100644 --- a/integration/dkg/dkg_whiteboard_test.go +++ b/integration/dkg/dkg_whiteboard_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "golang.org/x/exp/slices" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -194,6 +196,9 @@ func TestWithWhiteboard(t *testing.T) { // we run the DKG protocol with N consensus nodes N := 10 bootstrapNodesInfo := unittest.PrivateNodeInfosFixture(N, unittest.WithRole(flow.RoleConsensus)) + slices.SortFunc(bootstrapNodesInfo, func(lhs, rhs bootstrap.NodeInfo) int { + return flow.IdentifierCanonical(lhs.NodeID, rhs.NodeID) + }) conIdentities := make(flow.IdentitySkeletonList, 0, len(bootstrapNodesInfo)) for _, identity := range bootstrapNodesInfo { conIdentities = append(conIdentities, &identity.Identity().IdentitySkeleton) diff --git a/integration/go.mod b/integration/go.mod index 119a5f5020d..80a72464af7 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -22,8 +22,8 @@ require ( github.com/libp2p/go-libp2p v0.32.2 github.com/onflow/cadence v1.0.0-preview.52 github.com/onflow/crypto v0.25.2 - github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4 - github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4 + github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af + github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af github.com/onflow/flow-emulator v1.0.0-preview.41.0.20240829134601-0be55d6970b5 github.com/onflow/flow-go v0.37.7-0.20240826193109-e211841b59f5 github.com/onflow/flow-go-sdk v1.0.0-preview.55 diff --git a/integration/go.sum b/integration/go.sum index 51bccf519f4..5433df5fa1a 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -2149,10 +2149,10 @@ github.com/onflow/cadence v1.0.0-preview.52/go.mod h1:7wvvecnAZtYOspLOS3Lh+FuAmM github.com/onflow/crypto v0.25.0/go.mod h1:C8FbaX0x8y+FxWjbkHy0Q4EASCDR9bSPWZqlpCLYyVI= github.com/onflow/crypto v0.25.2 h1:GjHunqVt+vPcdqhxxhAXiMIF3YiLX7gTuTR5O+VG2ns= github.com/onflow/crypto v0.25.2/go.mod h1:fY7eLqUdMKV8EGOw301unP8h7PvLVy8/6gVR++/g0BY= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4 h1:pO1N4b/v2t/yRmeSfVkjYfrDDtFwvjsHgVgN+FRDQsM= -github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240728154212-5aafe7f667b4/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= -github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4 h1:Fr1QYriOtBDgH1BLYKp1rPONndZ5qz8LsNK98LAT+YI= -github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240728154212-5aafe7f667b4/go.mod h1:NgbMOYnMh0GN48VsNKZuiwK7uyk38Wyo8jN9+C9QE30= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af h1:1vg6OyFMigNoyqk4SWaMlfIr5POiFX9SpFpcCvrKrUc= +github.com/onflow/flow-core-contracts/lib/go/contracts v1.3.2-0.20240917184822-af7e508a44af/go.mod h1:u/mkP/B+PbV33tEG3qfkhhBlydSvAKxfLZSfB4lsJHg= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af h1:Vzaw1OSMOKnS3zGVyv0kgDRX+Owsj/IF4B0vbEO7kOk= +github.com/onflow/flow-core-contracts/lib/go/templates v1.3.2-0.20240917184822-af7e508a44af/go.mod h1:l8eCazFlUva+sQzh5hBWfOtk+iLmVYJ2DuhR2jSP06o= github.com/onflow/flow-emulator v1.0.0-preview.41.0.20240829134601-0be55d6970b5 h1:Z5PC3Sqvl2UemY27uwUwzkLb8EXUf+m/aEimxFzOXuc= github.com/onflow/flow-emulator v1.0.0-preview.41.0.20240829134601-0be55d6970b5/go.mod h1:gFafyGD4Qxs+XT2BRSIjQJ86ILSmgm1VYUoCr1nVxVI= github.com/onflow/flow-ft/lib/go/contracts v1.0.0 h1:mToacZ5NWqtlWwk/7RgIl/jeKB/Sy/tIXdw90yKHcV0= diff --git a/module/dkg.go b/module/dkg.go index 23d783037e0..412a8b71235 100644 --- a/module/dkg.go +++ b/module/dkg.go @@ -32,12 +32,28 @@ type DKGContractClient interface { // messages for that phase. ReadBroadcast(fromIndex uint, referenceBlock flow.Identifier) ([]messages.BroadcastDKGMessage, error) - // SubmitResult submits the final public result of the DKG protocol. This - // represents the group public key and the node's local computation of the - // public keys for each DKG participant. - // - // SubmitResult must be called strictly after the final phase has ended. - SubmitResult(crypto.PublicKey, []crypto.PublicKey) error + // SubmitParametersAndResult posts the DKG setup parameters (`flow.DKGIndexMap`) and the node's locally-computed DKG result to + // the DKG white-board smart contract. The DKG result are the group public key and the node's local computation of the public + // keys for each DKG participant. Serialized public keys are encoded as hex. + // Conceptually the flow.DKGIndexMap is not and output of the DKG protocol. Rather, it is part of the configuration/initialization + // information of the DKG. Before an epoch transition on the happy path (using the data in the EpochSetup event), each consensus + // participant locally fixes the DKG committee 𝒟 including the order of the respective nodes order to be identical to the consensus + // committee 𝒞. However, in case of a failed epoch transition, we desire the ability to manually provide the result of a successful + // DKG for the immediately next epoch (so-called recovery epoch). The DKG committee 𝒟 must have a sufficiently large overlap with + // the recovery epoch's consensus committee 𝒞 -- though for flexibility, we do *not* want to require that both committees are identical. + // Therefore, we need to explicitly specify the DKG committee 𝒟 on the fallback path. For uniformity of implementation, we do the + // same also on the happy path. + SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupPublicKey crypto.PublicKey, publicKeys []crypto.PublicKey) error + + // SubmitEmptyResult submits an empty result of the DKG protocol. The empty result is obtained by a node when + // it realizes locally that its DKG participation was unsuccessful (either because the DKG failed as a whole, + // or because the node received too many byzantine inputs). However, a node obtaining an empty result can + // happen in both cases of the DKG succeeding or failing. For further details, please see: + // https://flowfoundation.notion.site/Random-Beacon-2d61f3b3ad6e40ee9f29a1a38a93c99c + // Honest nodes would call `SubmitEmptyResult` strictly after the final phase has ended if DKG has ended. + // Though, `SubmitEmptyResult` also supports implementing byzantine participants for testing that submit an + // empty result too early (intentional protocol violation), *before* the final DKG phase concluded. + SubmitEmptyResult() error } // DKGController controls the execution of a Joint Feldman DKG instance. diff --git a/module/dkg/broker.go b/module/dkg/broker.go index e4fe3f4cad3..e230e0ba7ec 100644 --- a/module/dkg/broker.go +++ b/module/dkg/broker.go @@ -59,7 +59,7 @@ type Broker struct { log zerolog.Logger unit *engine.Unit dkgInstanceID string // unique identifier of the current dkg run (prevent replay attacks) - committee flow.IdentitySkeletonList // identities of DKG members + committee flow.IdentitySkeletonList // identities of DKG members in canonical order me module.Local // used for signing broadcast messages myIndex int // index of this instance in the committee dkgContractClients []module.DKGContractClient // array of clients to communicate with the DKG smart contract in priority order for fallbacks during retries @@ -81,6 +81,7 @@ var _ module.DKGBroker = (*Broker)(nil) // NewBroker instantiates a new epoch-specific broker capable of communicating // with other nodes via a network engine and dkg smart-contract. +// No errors are expected during normal operations. func NewBroker( log zerolog.Logger, dkgInstanceID string, @@ -90,13 +91,17 @@ func NewBroker( dkgContractClients []module.DKGContractClient, tunnel *BrokerTunnel, opts ...BrokerOpt, -) *Broker { +) (*Broker, error) { config := DefaultBrokerConfig() for _, apply := range opts { apply(&config) } + if !committee.Sorted(flow.Canonical[flow.IdentitySkeleton]) { + return nil, fmt.Errorf("DKG broker expects that participants are sorted in canonical order") + } + b := &Broker{ config: config, log: log.With().Str("component", "dkg_broker").Str("dkg_instance_id", dkgInstanceID).Logger(), @@ -114,7 +119,7 @@ func NewBroker( go b.listen() - return b + return b, nil } /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -204,18 +209,38 @@ func (b *Broker) Broadcast(data []byte) { } // SubmitResult publishes the result of the DKG protocol to the smart contract. +// This function should be passed the beacon keys (group key, participant keys) resulting from the DKG process. +// If the DKG process failed, no beacon keys will exist. In that case, we pass in nil here for both arguments. +// +// If non-nil arguments are provided, we submit a non-empty ResultSubmission to the DKG smart contract, +// indicating that we completed the DKG successfully and essentially "voting for" our result. +// If nil arguments are provided, we submit an empty ResultSubmission to the DKG smart contract, +// indicating that we completed the DKG unsuccessfully. func (b *Broker) SubmitResult(groupKey crypto.PublicKey, pubKeys []crypto.PublicKey) error { - // If the DKG failed locally, we will get a nil key vector here. We need to convert - // the nil slice to a slice of nil keys before submission. + // If the DKG failed locally, we will get a nil group key and nil participant key vector here. + // There are two different transaction templates for submitting either a happy-path and failure-path result. + // We use SubmitResult to submit a successful result and SubmitEmptyResult to communicate that we completed the DKG without a result. // // In general, if pubKeys does not have one key per participant, we cannot submit // a valid result - therefore we submit a nil vector (indicating that we have // completed the process, but we know that we don't have a valid result). - if len(pubKeys) != len(b.committee) { - b.log.Warn().Msgf("submitting dkg result with incomplete key vector (len=%d, expected=%d)", len(pubKeys), len(b.committee)) - // create a key vector with one nil entry for each committee member - pubKeys = make([]crypto.PublicKey, len(b.committee)) + var submitResult func(client module.DKGContractClient) error + if len(pubKeys) == len(b.committee) && groupKey != nil { + indexMap := make(flow.DKGIndexMap, len(pubKeys)) + // build a map of node IDs to indices in the key vector, + // this logic expects that committee is sorted in canonical order! + for i, participant := range b.committee { + indexMap[participant.NodeID] = i + } + submitResult = func(client module.DKGContractClient) error { + return client.SubmitParametersAndResult(indexMap, groupKey, pubKeys) + } + } else { + b.log.Warn().Msgf("submitting empty dkg result because I completed the DKG unsuccessfully") + submitResult = func(client module.DKGContractClient) error { + return client.SubmitEmptyResult() + } } backoff := retry.NewExponential(b.config.RetryInitialWait) @@ -228,10 +253,9 @@ func (b *Broker) SubmitResult(groupKey crypto.PublicKey, pubKeys []crypto.Public b.log.Warn().Msgf("submit result: retrying on attempt (%d) with fallback access node at index (%d)", totalAttempts, clientIndex) } backoff = retrymiddleware.AfterConsecutiveFailures(b.config.RetryMaxConsecutiveFailures, backoff, onMaxConsecutiveRetries) - attempts := 1 err := retry.Do(b.unit.Ctx(), backoff, func(ctx context.Context) error { - err := dkgContractClient.SubmitResult(groupKey, pubKeys) + err := submitResult(dkgContractClient) if err != nil { b.log.Error().Err(err).Msgf("error submitting DKG result, retrying (attempt %d)", attempts) attempts++ diff --git a/module/dkg/broker_test.go b/module/dkg/broker_test.go index 95d517e5e01..9f42648c2b7 100644 --- a/module/dkg/broker_test.go +++ b/module/dkg/broker_test.go @@ -28,11 +28,12 @@ var ( ) func initCommittee(n int) (identities flow.IdentitySkeletonList, locals []module.Local) { - privateStakingKeys := unittest.StakingKeys(n) - for i, key := range privateStakingKeys { - id := unittest.IdentityFixture(unittest.WithStakingPubKey(key.PublicKey())) - identities = append(identities, &id.IdentitySkeleton) - local, _ := local.New(id.IdentitySkeleton, privateStakingKeys[i]) + for _, identity := range unittest.IdentityListFixture(n).ToSkeleton().Sort(flow.Canonical[flow.IdentitySkeleton]) { + stakingPriv := unittest.StakingPrivKeyFixture() + identity.StakingPubKey = stakingPriv.PublicKey() + + identities = append(identities, identity) + local, _ := local.New(*identity, stakingPriv) locals = append(locals, local) } return identities, locals @@ -62,6 +63,23 @@ func TestDefaultConfig(t *testing.T) { }) } +// TestNewBroker_OrderNotCanonical checks that broker creation fails if the identities aren't sorted in canonical order. +func TestNewBroker_OrderNotCanonical(t *testing.T) { + identities, locals := initCommittee(2) + identities[0], identities[1] = identities[1], identities[0] // break canonical order + + _, err := NewBroker( + zerolog.Logger{}, + dkgInstanceID, + identities, + locals[orig], + orig, + []module.DKGContractClient{&mock.DKGContractClient{}}, + NewBrokerTunnel(), + ) + require.Error(t, err) +} + // TestPrivateSend_Valid checks that the broker correctly converts the message // destination parameter (index in committee list) to the corresponding // public Identifier, and successfully sends a DKG message to the intended @@ -70,7 +88,7 @@ func TestPrivateSend_Valid(t *testing.T) { committee, locals := initCommittee(2) // sender broker - sender := NewBroker( + sender, err := NewBroker( zerolog.Logger{}, dkgInstanceID, committee, @@ -79,6 +97,7 @@ func TestPrivateSend_Valid(t *testing.T) { []module.DKGContractClient{&mock.DKGContractClient{}}, NewBrokerTunnel(), ) + require.NoError(t, err) // expected DKGMessageOut expectedMsg := msg.PrivDKGMessageOut{ @@ -111,7 +130,7 @@ func TestPrivateSend_IndexOutOfRange(t *testing.T) { committee, locals := initCommittee(2) // sender broker - sender := NewBroker( + sender, err := NewBroker( zerolog.Logger{}, dkgInstanceID, committee, @@ -120,6 +139,7 @@ func TestPrivateSend_IndexOutOfRange(t *testing.T) { []module.DKGContractClient{&mock.DKGContractClient{}}, NewBrokerTunnel(), ) + require.NoError(t, err) // Launch a background routine to capture messages sent through the tunnel. // No messages should be received because we are only sending invalid ones. @@ -145,7 +165,7 @@ func TestReceivePrivateMessage_Valid(t *testing.T) { committee, locals := initCommittee(2) // receiving broker - receiver := NewBroker( + receiver, err := NewBroker( zerolog.Logger{}, dkgInstanceID, committee, @@ -154,6 +174,7 @@ func TestReceivePrivateMessage_Valid(t *testing.T) { []module.DKGContractClient{&mock.DKGContractClient{}}, NewBrokerTunnel(), ) + require.NoError(t, err) dkgMessage := msg.NewDKGMessage(msgb, dkgInstanceID) expectedMsg := msg.PrivDKGMessageIn{ @@ -192,7 +213,7 @@ func TestBroadcastMessage(t *testing.T) { committee, locals := initCommittee(2) // sender - sender := NewBroker( + sender, err := NewBroker( unittest.Logger(), dkgInstanceID, committee, @@ -202,6 +223,7 @@ func TestBroadcastMessage(t *testing.T) { NewBrokerTunnel(), func(config *BrokerConfig) { config.RetryInitialWait = 1 }, // disable waiting between retries for tests ) + require.NoError(t, err) expectedMsg, err := sender.prepareBroadcastMessage(msgb) require.NoError(t, err) @@ -236,7 +258,7 @@ func TestBroadcastMessage(t *testing.T) { func TestPoll(t *testing.T) { committee, locals := initCommittee(2) - sender := NewBroker( + sender, err := NewBroker( zerolog.Logger{}, dkgInstanceID, committee, @@ -245,8 +267,9 @@ func TestPoll(t *testing.T) { []module.DKGContractClient{&mock.DKGContractClient{}}, NewBrokerTunnel(), ) + require.NoError(t, err) - recipient := NewBroker( + recipient, err := NewBroker( zerolog.Logger{}, dkgInstanceID, committee, @@ -255,6 +278,7 @@ func TestPoll(t *testing.T) { []module.DKGContractClient{&mock.DKGContractClient{}}, NewBrokerTunnel(), ) + require.NoError(t, err) blockID := unittest.IdentifierFixture() bcastMsgs := []msg.BroadcastDKGMessage{} @@ -286,7 +310,7 @@ func TestPoll(t *testing.T) { } }() - err := sender.Poll(blockID) + err = sender.Poll(blockID) require.NoError(t, err) // check that the contract has been correctly called @@ -315,7 +339,7 @@ func TestLogHook(t *testing.T) { logger := zerolog.New(os.Stdout).Level(zerolog.WarnLevel).Hook(hook) // sender - sender := NewBroker( + sender, err := NewBroker( logger, dkgInstanceID, committee, @@ -324,6 +348,7 @@ func TestLogHook(t *testing.T) { []module.DKGContractClient{&mock.DKGContractClient{}}, NewBrokerTunnel(), ) + require.NoError(t, err) sender.Disqualify(1, "testing") sender.FlagMisbehavior(1, "test") @@ -336,7 +361,7 @@ func TestProcessPrivateMessage_InvalidOrigin(t *testing.T) { committee, locals := initCommittee(2) // receiving broker - receiver := NewBroker( + receiver, err := NewBroker( zerolog.Logger{}, dkgInstanceID, committee, @@ -345,6 +370,7 @@ func TestProcessPrivateMessage_InvalidOrigin(t *testing.T) { []module.DKGContractClient{&mock.DKGContractClient{}}, NewBrokerTunnel(), ) + require.NoError(t, err) // Launch a background routine to capture messages forwared to the private // message channel. No messages should be received because we are only diff --git a/module/dkg/client.go b/module/dkg/client.go index bee6a6573dd..f20b391a836 100644 --- a/module/dkg/client.go +++ b/module/dkg/client.go @@ -30,6 +30,8 @@ type Client struct { env templates.Environment } +var _ module.DKGContractClient = (*Client)(nil) + // NewClient initializes a new client to the Flow DKG contract func NewClient( log zerolog.Logger, @@ -178,11 +180,18 @@ func (c *Client) Broadcast(msg model.BroadcastDKGMessage) error { return nil } -// SubmitResult submits the final public result of the DKG protocol. This -// represents the group public key and the node's local computation of the -// public keys for each DKG participant. Serialized pub keys are encoded as hex. -func (c *Client) SubmitResult(groupPublicKey crypto.PublicKey, publicKeys []crypto.PublicKey) error { - +// SubmitParametersAndResult posts the DKG setup parameters (`flow.DKGIndexMap`) and the node's locally-computed DKG result to +// the DKG white-board smart contract. The DKG result are the group public key and the node's local computation of the public +// keys for each DKG participant. Serialized public keys are encoded as hex. +// Conceptually the flow.DKGIndexMap is not and output of the DKG protocol. Rather, it is part of the configuration/initialization +// information of the DKG. Before an epoch transition on the happy path (using the data in the EpochSetup event), each consensus +// participant locally fixes the DKG committee 𝒟 including the order of the respective nodes order to be identical to the consensus +// committee 𝒞. However, in case of a failed epoch transition, we desire the ability to manually provide the result of a successful +// DKG for the immediately next epoch (so-called recovery epoch). The DKG committee 𝒟 must have a sufficiently large overlap with +// the recovery epoch's consensus committee 𝒞 -- though for flexibility, we do *not* want to require that both committees are identical. +// Therefore, we need to explicitly specify the DKG committee 𝒟 on the fallback path. For uniformity of implementation, we do the +// same also on the happy path. +func (c *Client) SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupPublicKey crypto.PublicKey, publicKeys []crypto.PublicKey) error { started := time.Now() ctx, cancel := context.WithTimeout(context.Background(), epochs.TransactionSubmissionTimeout) defer cancel() @@ -207,38 +216,45 @@ func (c *Client) SubmitResult(groupPublicKey crypto.PublicKey, publicKeys []cryp SetPayer(account.Address). AddAuthorizer(account.Address) - // Note: We need to make sure that we pull the keys out in the same order that - // we have done here. Group Public key first followed by the individual public keys - finalSubmission := make([]cadence.Value, 0, len(publicKeys)) + trimmedGroupHexString := trim0x(groupPublicKey.String()) + cdcGroupString, err := cadence.NewString(trimmedGroupHexString) + if err != nil { + return fmt.Errorf("could not convert group key to cadence: %w", err) + } - // first append group public key - if groupPublicKey != nil { - trimmedGroupHexString := trim0x(groupPublicKey.String()) - cdcGroupString, err := cadence.NewString(trimmedGroupHexString) - if err != nil { - return fmt.Errorf("could not convert group key to cadence: %w", err) - } - finalSubmission = append(finalSubmission, cadence.NewOptional(cdcGroupString)) - } else { - finalSubmission = append(finalSubmission, cadence.NewOptional(nil)) + // setup first arg - group key + err = tx.AddArgument(cdcGroupString) + if err != nil { + return fmt.Errorf("could not add argument to transaction: %w", err) } + cdcPublicKeys := make([]cadence.Value, 0, len(publicKeys)) for _, publicKey := range publicKeys { - // append individual public keys - if publicKey != nil { - trimmedHexString := trim0x(publicKey.String()) - cdcPubKey, err := cadence.NewString(trimmedHexString) - if err != nil { - return fmt.Errorf("could not convert pub keyshare to cadence: %w", err) - } - finalSubmission = append(finalSubmission, cadence.NewOptional(cdcPubKey)) - } else { - finalSubmission = append(finalSubmission, cadence.NewOptional(nil)) + trimmedHexString := trim0x(publicKey.String()) + cdcPubKey, err := cadence.NewString(trimmedHexString) + if err != nil { + return fmt.Errorf("could not convert pub keyshare to cadence: %w", err) } + cdcPublicKeys = append(cdcPublicKeys, cdcPubKey) } - err = tx.AddArgument(cadence.NewArray(finalSubmission)) + // setup second arg - array of public keys + err = tx.AddArgument(cadence.NewArray(cdcPublicKeys)) + if err != nil { + return fmt.Errorf("could not add argument to transaction: %w", err) + } + + cdcIndexMap := make([]cadence.KeyValuePair, 0, len(indexMap)) + for nodeID, dkgIndex := range indexMap { + cdcIndexMap = append(cdcIndexMap, cadence.KeyValuePair{ + Key: cadence.String(nodeID.String()), + Value: cadence.NewInt(dkgIndex), + }) + } + + // setup third arg - IndexMap + err = tx.AddArgument(cadence.NewDictionary(cdcIndexMap)) if err != nil { return fmt.Errorf("could not add argument to transaction: %w", err) } @@ -263,6 +279,60 @@ func (c *Client) SubmitResult(groupPublicKey crypto.PublicKey, publicKeys []cryp return nil } +// SubmitEmptyResult submits an empty result of the DKG protocol. The empty result is obtained by a node when +// it realizes locally that its DKG participation was unsuccessful (either because the DKG failed as a whole, +// or because the node received too many byzantine inputs). However, a node obtaining an empty result can +// happen in both cases of the DKG succeeding or failing. For further details, please see: +// https://flowfoundation.notion.site/Random-Beacon-2d61f3b3ad6e40ee9f29a1a38a93c99c +// Honest nodes would call `SubmitEmptyResult` strictly after the final phase has ended if DKG has ended. +// Though, `SubmitEmptyResult` also supports implementing byzantine participants for testing that submit an +// empty result too early (intentional protocol violation), *before* the final DKG phase concluded. +// SubmitEmptyResult must be called strictly after the final phase has ended if DKG has failed. +func (c *Client) SubmitEmptyResult() error { + started := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), epochs.TransactionSubmissionTimeout) + defer cancel() + + // get account for given address + account, err := c.GetAccount(ctx) + if err != nil { + return fmt.Errorf("could not get account details: %w", err) + } + + // get latest finalized block to execute transaction + latestBlock, err := c.FlowClient.GetLatestBlock(ctx, false) + if err != nil { + return fmt.Errorf("could not get latest block from node: %w", err) + } + + tx := sdk.NewTransaction(). + SetScript(templates.GenerateSendEmptyDKGFinalSubmissionScript(c.env)). + SetComputeLimit(9999). + SetReferenceBlockID(latestBlock.ID). + SetProposalKey(account.Address, c.AccountKeyIndex, account.Keys[int(c.AccountKeyIndex)].SequenceNumber). + SetPayer(account.Address). + AddAuthorizer(account.Address) + + // sign envelope using account signer + err = tx.SignEnvelope(account.Address, c.AccountKeyIndex, c.Signer) + if err != nil { + return fmt.Errorf("could not sign transaction: %w", err) + } + + c.Log.Info().Str("tx_id", tx.ID().Hex()).Msg("sending SubmitEmptyResult transaction") + txID, err := c.SendTransaction(ctx, tx) + if err != nil { + return fmt.Errorf("failed to submit transaction: %w", err) + } + + err = c.WaitForSealed(ctx, txID, started) + if err != nil { + return fmt.Errorf("failed to wait for transaction seal: %w", err) + } + + return nil +} + // trim0x trims the `0x` if it exists from a hexadecimal string // This method is required as the DKG contract expects key lengths of 192 bytes // the `PublicKey.String()` method returns the hexadecimal string representation of the diff --git a/module/dkg/controller.go b/module/dkg/controller.go index e37924c6e81..34f91b49a72 100644 --- a/module/dkg/controller.go +++ b/module/dkg/controller.go @@ -51,6 +51,8 @@ type Controller struct { once *sync.Once } +var _ module.DKGController = (*Controller)(nil) + // NewController instantiates a new Joint Feldman DKG controller. func NewController( log zerolog.Logger, diff --git a/module/dkg/controller_factory.go b/module/dkg/controller_factory.go index 5b3a015c95b..13587835e43 100644 --- a/module/dkg/controller_factory.go +++ b/module/dkg/controller_factory.go @@ -42,17 +42,24 @@ func NewControllerFactory( // Create creates a new epoch-specific Controller equipped with a broker which // is capable of communicating with other nodes. +// Participants list must be sorted in canonical order. +// No errors are expected during normal operations. func (f *ControllerFactory) Create( dkgInstanceID string, participants flow.IdentitySkeletonList, - seed []byte) (module.DKGController, error) { + seed []byte, +) (module.DKGController, error) { + // ensure participants are sorted in canonical order + if !participants.Sorted(flow.Canonical[flow.IdentitySkeleton]) { + return nil, fmt.Errorf("participants are not sorted in canonical order") + } myIndex, ok := participants.GetIndex(f.me.NodeID()) if !ok { return nil, fmt.Errorf("failed to create controller factory, node %s is not part of DKG committee", f.me.NodeID().String()) } - broker := NewBroker( + broker, err := NewBroker( f.log, dkgInstanceID, participants, @@ -61,6 +68,9 @@ func (f *ControllerFactory) Create( f.dkgContractClients, f.tunnel, ) + if err != nil { + return nil, fmt.Errorf("could not create DKG broker: %w", err) + } n := len(participants) threshold := signature.RandomBeaconThreshold(n) diff --git a/module/dkg/mock_client.go b/module/dkg/mock_client.go deleted file mode 100644 index 0f05f14f34d..00000000000 --- a/module/dkg/mock_client.go +++ /dev/null @@ -1,44 +0,0 @@ -package dkg - -import ( - "github.com/onflow/crypto" - "github.com/rs/zerolog" - - "github.com/onflow/flow-go/model/flow" - model "github.com/onflow/flow-go/model/messages" -) - -// TEMPORARY: The functionality to allow starting up a node without a properly configured -// machine account is very much intended to be temporary. - -// This is required to support the very first mainnet spork with the epoch smart contract. -// At the beginning of the spork, operators will not have been able to generate their machine account -// because the smart contracts to do so have not been deployed yet. Therefore, for the duration of the spork, -// we allow this config to be omitted. For all subsequent sporks, it will be required. -// Implemented by: https://github.com/dapperlabs/flow-go/issues/5585 -// Will be reverted by: https://github.com/dapperlabs/flow-go/issues/5619 - -type MockClient struct { - log zerolog.Logger -} - -func NewMockClient(log zerolog.Logger) *MockClient { - - log = log.With().Str("component", "mock_dkg_contract_client").Logger() - return &MockClient{log: log} -} - -func (c *MockClient) Broadcast(msg model.BroadcastDKGMessage) error { - c.log.Fatal().Msg("caution: missing machine account configuration, but machine account used (Broadcast)") - return nil -} - -func (c *MockClient) ReadBroadcast(fromIndex uint, referenceBlock flow.Identifier) ([]model.BroadcastDKGMessage, error) { - c.log.Fatal().Msg("caution: missing machine account configuration, but machine account used (ReadBroadcast)") - return nil, nil -} - -func (c *MockClient) SubmitResult(groupPublicKey crypto.PublicKey, publicKeys []crypto.PublicKey) error { - c.log.Fatal().Msg("caution: missing machine account configuration, but machine account used (SubmitResult)") - return nil -} diff --git a/module/mock/dkg_contract_client.go b/module/mock/dkg_contract_client.go index 3f5c6e72ed3..3d2952542a9 100644 --- a/module/mock/dkg_contract_client.go +++ b/module/mock/dkg_contract_client.go @@ -64,17 +64,35 @@ func (_m *DKGContractClient) ReadBroadcast(fromIndex uint, referenceBlock flow.I return r0, r1 } -// SubmitResult provides a mock function with given fields: _a0, _a1 -func (_m *DKGContractClient) SubmitResult(_a0 crypto.PublicKey, _a1 []crypto.PublicKey) error { - ret := _m.Called(_a0, _a1) +// SubmitEmptyResult provides a mock function with given fields: +func (_m *DKGContractClient) SubmitEmptyResult() error { + ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for SubmitResult") + panic("no return value specified for SubmitEmptyResult") } var r0 error - if rf, ok := ret.Get(0).(func(crypto.PublicKey, []crypto.PublicKey) error); ok { - r0 = rf(_a0, _a1) + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitParametersAndResult provides a mock function with given fields: indexMap, groupPublicKey, publicKeys +func (_m *DKGContractClient) SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupPublicKey crypto.PublicKey, publicKeys []crypto.PublicKey) error { + ret := _m.Called(indexMap, groupPublicKey, publicKeys) + + if len(ret) == 0 { + panic("no return value specified for SubmitParametersAndResult") + } + + var r0 error + if rf, ok := ret.Get(0).(func(flow.DKGIndexMap, crypto.PublicKey, []crypto.PublicKey) error); ok { + r0 = rf(indexMap, groupPublicKey, publicKeys) } else { r0 = ret.Error(0) } diff --git a/utils/unittest/execution_state.go b/utils/unittest/execution_state.go index 85bbd6d5df3..91c1a905cb7 100644 --- a/utils/unittest/execution_state.go +++ b/utils/unittest/execution_state.go @@ -23,7 +23,7 @@ const ServiceAccountPrivateKeySignAlgo = crypto.ECDSAP256 const ServiceAccountPrivateKeyHashAlgo = hash.SHA2_256 // Pre-calculated state commitment with root account with the above private key -const GenesisStateCommitmentHex = "9d3d647e52b92f4854efba443b6385de20ff8898a3ac1d13f4c7b984d726d6ff" +const GenesisStateCommitmentHex = "f2feb056edff66df9c50fb65c398f23f02f6b94b81f062971184c68b1339bef9" var GenesisStateCommitment flow.StateCommitment @@ -87,10 +87,10 @@ func genesisCommitHexByChainID(chainID flow.ChainID) string { return GenesisStateCommitmentHex } if chainID == flow.Testnet { - return "fd488e56bbcac397de5da66effd0be7d6ea818e9ade0988a965c86e950930af9" + return "3d1a1f14ce2d1e6b976869405a42b9db48eaca4b0b19ecf9389407453c970bda" } if chainID == flow.Sandboxnet { return "e1c08b17f9e5896f03fe28dd37ca396c19b26628161506924fbf785834646ea1" } - return "a4a4888d31e1fbad51dfba99e922ceb71e483624b8abc101ec14a501dfdd4a83" + return "f128486b7f9301af671c76cf49c15572c0344e287d2f8144294fde248243e24f" }