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

DKG Result Submission Backward Compatibility #6827

Open
wants to merge 3 commits into
base: feature/efm-recovery
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration/localnet/builder/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func init() {
flag.Uint64Var(&numViewsEpoch, "epoch-length", 10000, "number of views in epoch")
flag.Uint64Var(&numViewsInStakingPhase, "epoch-staking-phase-length", 2000, "number of views in epoch staking phase")
flag.Uint64Var(&numViewsInDKGPhase, "epoch-dkg-phase-length", 2000, "number of views in epoch dkg phase")
flag.Uint64Var(&finalizationSafetyThreshold, "epoch-commit-safety-threshold", 1000, "number of views for safety threshold T (assume: one finalization occurs within T blocks)")
flag.Uint64Var(&finalizationSafetyThreshold, "finalization-safety-threshold", 1000, "number of views for safety threshold T (assume: one finalization occurs within T blocks)")
flag.BoolVar(&profiler, "profiler", DefaultProfiler, "whether to enable the auto-profiler")
flag.BoolVar(&profileUploader, "profile-uploader", DefaultProfileUploader, "whether to upload profiles to the cloud")
flag.BoolVar(&tracing, "tracing", DefaultTracing, "whether to enable low-overhead tracing in flow")
Expand Down
140 changes: 139 additions & 1 deletion module/dkg/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package dkg

import (
"context"
_ "embed"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"

"github.com/rs/zerolog"
Expand All @@ -22,6 +24,17 @@ import (
"github.com/onflow/flow-go/module/epochs"
)

// submitResultV1Script is a version of the DKG result submission script compatible with
// Protocol Version 1 (omitting DKG index map field).
// Deprecated:
// TODO(mainnet27, #6792): remove
//
//go:embed dkg_submit_result_v1.cdc
var submitResultV1Script string

//go:embed dkg_check_v2.cdc
var checkV2Script string

// Client is a client to the Flow DKG contract. Allows functionality to Broadcast,
// read a Broadcast and submit the final result of the DKG protocol
type Client struct {
Expand Down Expand Up @@ -180,6 +193,115 @@ func (c *Client) Broadcast(msg model.BroadcastDKGMessage) error {
return nil
}

// submitResult_ProtocolV1 submits this node's locally computed DKG result (public key vector)
// to the FlowDKG smart contract, using the submission API associated with Protocol Version 1.
// Input public keys are expected to be either all nil or all non-nil.
// Deprecated: This function is a temporary measure to provide backward compatibility between Protocol Versions 1 and 2.
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
// Deprecated: This function is a temporary measure to provide backward compatibility between Protocol Versions 1 and 2.
// Deprecated: This is a temporary function to provide backward compatibility between Protocol Versions 1 and 2.

// Protocol Version 2 introduces the DKG index map, which must be included in result submissions (see SubmitParametersAndResult).
// TODO(mainnet27, #6792): remove this function
func (c *Client) submitResult_ProtocolV1(groupPublicKey crypto.PublicKey, publicKeys []crypto.PublicKey) 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([]byte(templates.ReplaceAddresses(submitResultV1Script, c.env))).
SetComputeLimit(9999).
SetReferenceBlockID(latestBlock.ID).
SetProposalKey(account.Address, c.AccountKeyIndex, account.Keys[int(c.AccountKeyIndex)].SequenceNumber).
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))

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

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))
}
}

err = tx.AddArgument(cadence.NewArray(finalSubmission))
if err != nil {
return fmt.Errorf("could not add argument to transaction: %w", err)
}

// 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 SubmitResult 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
}

// checkDKGContractV2Compatible executes a script which imports a type only defined in the
// FlowDKG smart contract version associated with Protocol Version 2.
// By observing the result of this script execution, we can infer the current FlowDKG version.
// Deprecated:
// TODO(mainnet27, #6792): remove this function
// Returns:
// - (true, nil) if the smart contract is certainly v2-compatible (script succeeds)
// - (false, nil) if the smart contract is certainly not v2-compatible (script fails with expected error)
// - (false, error) if we don't know whether the smart contract is v2-compatible (script fails with unexpected error)
func (c *Client) checkDKGContractV2Compatible(ctx context.Context) (bool, error) {
script := []byte(templates.ReplaceAddresses(checkV2Script, c.env))
_, err := c.FlowClient.ExecuteScriptAtLatestBlock(ctx, script, nil)
if err == nil {
return true, nil
}
v1ErrorSubstring := "cannot find type in this scope: `FlowDKG.ResultSubmission`"
if strings.Contains(err.Error(), v1ErrorSubstring) {
return false, nil
}
return false, fmt.Errorf("unable to determine FlowDKG compatibility: %w", err)
}

// 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 results are the node's local computation of the group public key and the public
// key shares. Serialized public keys are encoded as lower-case hex strings.
Expand All @@ -196,6 +318,22 @@ func (c *Client) SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupPubli
ctx, cancel := context.WithTimeout(context.Background(), epochs.TransactionSubmissionTimeout)
defer cancel()

// TODO(mainnet27, #6792): remove this code block
{
v2Compatible, err := c.checkDKGContractV2Compatible(ctx)
// CASE 1: we know FlowDKG is not v2 compatible - fallback to v1 submission
if !v2Compatible && err == nil {
c.Log.Debug().Msg("detected FlowDKG version incompatible with protocol v2 - falling back to v1 result submission")
return c.submitResult_ProtocolV1(groupPublicKey, publicKeys)
}
// CASE 2: we aren't sure whether FlowDKG is v2 compatible - fallback to v1 submission
if err != nil {
c.Log.Warn().Err(err).Msg("unable to determine FlowDKG compatibility - falling back to v1 result submission")
return c.submitResult_ProtocolV1(groupPublicKey, publicKeys)
}
Comment on lines +329 to +333
Copy link
Member

Choose a reason for hiding this comment

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

This is probably not a question that we can address as part of this PR. When reading these lines, I am wondering what would happen if the protocol was on version 2 already, but script execution failed and we would submit with version 1 ... not sure how unlikely that is and what the consequences could be. Probably something best to be discussed in person.

// CASE 3: FlowDKG is compatible with v2 - proceed with happy path logic
}

// get account for given address
account, err := c.GetAccount(ctx)
if err != nil {
Expand Down Expand Up @@ -265,7 +403,7 @@ func (c *Client) SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupPubli
return fmt.Errorf("could not sign transaction: %w", err)
}

c.Log.Info().Str("tx_id", tx.ID().Hex()).Msg("sending SubmitResult transaction")
c.Log.Info().Str("tx_id", tx.ID().Hex()).Msg("sending SubmitParametersAndResult transaction")
txID, err := c.SendTransaction(ctx, tx)
if err != nil {
return fmt.Errorf("failed to submit transaction: %w", err)
Expand Down
11 changes: 11 additions & 0 deletions module/dkg/dkg_check_v2.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import FlowDKG from "FlowDKG"

// This script is used to detect whether the deployed FlowDKG instance is compatible with Protocol Version 2
// ResultSubmission is a type only defined in the v2 contract version, so this script will:
// - return [] when executing against a v2 contract
// - return a predictable error when executing against a v1 contract
//
// TODO(mainnet27, #6792): remove this file
access(all) fun main(): [FlowDKG.ResultSubmission] {
return []
}
18 changes: 18 additions & 0 deletions module/dkg/dkg_submit_result_v1.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import FlowDKG from "FlowDKG"

// This transaction submits the DKG result using the contract API compatible with Protocol Version 1.
// It is included as a backward compatibility measure during the transition to Protocol Version 2.
// TODO(mainnet27, #6792): remove this file
transaction(submission: [String?]) {

let dkgParticipant: &FlowDKG.Participant

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

execute {
self.dkgParticipant.sendFinalSubmission(submission)
}
}
Loading