// tbtc_dkg.go: DKG lifecycle, result assembly and validation for the TbtcChain adapter. package ethereum import ( "crypto/ecdsa" "fmt" "math/big" "reflect" "sort" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" "github.com/keep-network/keep-core/pkg/chain" ecdsaabi "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/abi" "github.com/keep-network/keep-core/pkg/crypto/secp256k1" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tbtc" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" ) func (tc *TbtcChain) OnDKGStarted( handler func(event *tbtc.DKGStartedEvent), ) subscription.EventSubscription { onEvent := func( seed *big.Int, blockNumber uint64, ) { handler(&tbtc.DKGStartedEvent{ Seed: seed, BlockNumber: blockNumber, }) } return tc.walletRegistry.DkgStartedEvent(nil, nil).OnEvent(onEvent) } func (tc *TbtcChain) PastDKGStartedEvents( filter *tbtc.DKGStartedEventFilter, ) ([]*tbtc.DKGStartedEvent, error) { var startBlock uint64 var endBlock *uint64 var seed []*big.Int if filter != nil { startBlock = filter.StartBlock endBlock = filter.EndBlock seed = filter.Seed } events, err := tc.walletRegistry.PastDkgStartedEvents( startBlock, endBlock, seed, ) if err != nil { return nil, err } dkgStartedEvents := make([]*tbtc.DKGStartedEvent, len(events)) for i, event := range events { dkgStartedEvents[i] = &tbtc.DKGStartedEvent{ Seed: event.Seed, BlockNumber: event.Raw.BlockNumber, } } sort.SliceStable(dkgStartedEvents, func(i, j int) bool { return dkgStartedEvents[i].BlockNumber < dkgStartedEvents[j].BlockNumber }) return dkgStartedEvents, err } func (tc *TbtcChain) OnDKGResultSubmitted( handler func(event *tbtc.DKGResultSubmittedEvent), ) subscription.EventSubscription { onEvent := func( resultHash [32]byte, seed *big.Int, result ecdsaabi.EcdsaDkgResult, blockNumber uint64, ) { tbtcResult, err := convertDkgResultFromAbiType(result) if err != nil { // Surface the raw event payload alongside the conversion // error so the bad event is recoverable from logs instead // of being silently discarded (the conversion failure // drops the event before the handler ever sees it). logger.Errorf( "unexpected DKG result in DKGResultSubmitted event "+ "resultHash=[0x%x] seed=[%v] blockNumber=[%d] "+ "submitterMemberIndex=[%v] signingMembersIndices=[%v]: [%v]", resultHash, seed, blockNumber, result.SubmitterMemberIndex, result.SigningMembersIndices, err, ) return } handler(&tbtc.DKGResultSubmittedEvent{ Seed: seed, ResultHash: resultHash, Result: tbtcResult, BlockNumber: blockNumber, }) } return tc.walletRegistry. DkgResultSubmittedEvent(nil, nil, nil). OnEvent(onEvent) } // convertDkgResultFromAbiType converts the WalletRegistry-specific DKG // result to the format applicable for the TBTC application. func convertDkgResultFromAbiType( result ecdsaabi.EcdsaDkgResult, ) (*tbtc.DKGChainResult, error) { if err := validateMemberIndex(result.SubmitterMemberIndex); err != nil { return nil, fmt.Errorf( "unexpected submitter member index: [%v]", err, ) } signingMembersIndexes := make( []group.MemberIndex, len(result.SigningMembersIndices), ) for i, memberIndex := range result.SigningMembersIndices { if err := validateMemberIndex(memberIndex); err != nil { return nil, fmt.Errorf( "unexpected signing member index: [%v]", err, ) } signingMembersIndexes[i] = group.MemberIndex(memberIndex.Uint64()) } return &tbtc.DKGChainResult{ SubmitterMemberIndex: group.MemberIndex(result.SubmitterMemberIndex.Uint64()), GroupPublicKey: result.GroupPubKey, MisbehavedMembersIndexes: result.MisbehavedMembersIndices, Signatures: result.Signatures, SigningMembersIndexes: signingMembersIndexes, Members: result.Members, MembersHash: result.MembersHash, }, nil } // convertDkgResultToAbiType converts the TBTC-specific DKG result to // the format applicable for the WalletRegistry ABI. func convertDkgResultToAbiType( result *tbtc.DKGChainResult, ) ecdsaabi.EcdsaDkgResult { signingMembersIndices := make([]*big.Int, len(result.SigningMembersIndexes)) for i, memberIndex := range result.SigningMembersIndexes { signingMembersIndices[i] = big.NewInt(int64(memberIndex)) } return ecdsaabi.EcdsaDkgResult{ SubmitterMemberIndex: big.NewInt(int64(result.SubmitterMemberIndex)), GroupPubKey: result.GroupPublicKey, MisbehavedMembersIndices: result.MisbehavedMembersIndexes, Signatures: result.Signatures, SigningMembersIndices: signingMembersIndices, Members: result.Members, MembersHash: result.MembersHash, } } // validateMemberIndex guards a *big.Int member index against both an // upper bound and a non-positive value. The non-positive check // (`chainMemberIndex.Sign() <= 0`) is a behavior change introduced // during the #4191 file split (the upper-bound check predated the // split). On-chain indices are 1-based and uint64, so the new check // is unreachable for valid events; it exists to surface a malformed // event as an error instead of producing a zero `group.MemberIndex`. func validateMemberIndex(chainMemberIndex *big.Int) error { maxMemberIndex := big.NewInt(group.MaxMemberIndex) if chainMemberIndex.Sign() <= 0 || chainMemberIndex.Cmp(maxMemberIndex) > 0 { return fmt.Errorf("invalid member index value: [%v]", chainMemberIndex) } return nil } func (tc *TbtcChain) OnDKGResultChallenged( handler func(event *tbtc.DKGResultChallengedEvent), ) subscription.EventSubscription { onEvent := func( resultHash [32]byte, challenger common.Address, reason string, blockNumber uint64, ) { handler(&tbtc.DKGResultChallengedEvent{ ResultHash: resultHash, Challenger: chain.Address(challenger.Hex()), Reason: reason, BlockNumber: blockNumber, }) } return tc.walletRegistry. DkgResultChallengedEvent(nil, nil, nil). OnEvent(onEvent) } func (tc *TbtcChain) OnDKGResultApproved( handler func(event *tbtc.DKGResultApprovedEvent), ) subscription.EventSubscription { onEvent := func( resultHash [32]byte, approver common.Address, blockNumber uint64, ) { handler(&tbtc.DKGResultApprovedEvent{ ResultHash: resultHash, Approver: chain.Address(approver.Hex()), BlockNumber: blockNumber, }) } return tc.walletRegistry. DkgResultApprovedEvent(nil, nil, nil). OnEvent(onEvent) } // AssembleDKGResult assembles the DKG chain result according to the rules // expected by the given chain. func (tc *TbtcChain) AssembleDKGResult( submitterMemberIndex group.MemberIndex, groupPublicKey *ecdsa.PublicKey, operatingMembersIndexes []group.MemberIndex, misbehavedMembersIndexes []group.MemberIndex, signatures map[group.MemberIndex][]byte, groupSelectionResult *tbtc.GroupSelectionResult, ) (*tbtc.DKGChainResult, error) { serializedGroupPublicKey, err := convertPubKeyToChainFormat(groupPublicKey) if err != nil { return nil, fmt.Errorf( "could not convert group public key to chain format: [%v]", err, ) } // Sort misbehavedMembersIndexes slice in ascending order as expected // by the on-chain contract. sort.Slice(misbehavedMembersIndexes[:], func(i, j int) bool { return misbehavedMembersIndexes[i] < misbehavedMembersIndexes[j] }) signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat( signatures, ) if err != nil { return nil, fmt.Errorf( "could not convert signatures to chain format: [%v]", err, ) } // Sort operatingOperatorsIDs slice in ascending order as the slice // holding the operators IDs used to compute the members hash is // expected to be sorted in the same way. sort.Slice(operatingMembersIndexes[:], func(i, j int) bool { return operatingMembersIndexes[i] < operatingMembersIndexes[j] }) operatingOperatorsIDs := make([]chain.OperatorID, len(operatingMembersIndexes)) for i, operatingMemberIndex := range operatingMembersIndexes { operatingOperatorsIDs[i] = groupSelectionResult.OperatorsIDs[operatingMemberIndex-1] } membersHash, err := computeOperatorsIDsHash(operatingOperatorsIDs) if err != nil { return nil, fmt.Errorf("could not compute members hash: [%v]", err) } return &tbtc.DKGChainResult{ SubmitterMemberIndex: submitterMemberIndex, GroupPublicKey: serializedGroupPublicKey[:], MisbehavedMembersIndexes: misbehavedMembersIndexes, Signatures: signatureBytes, SigningMembersIndexes: signingMemberIndices, Members: groupSelectionResult.OperatorsIDs, MembersHash: membersHash, }, nil } func (tc *TbtcChain) SubmitDKGResult( dkgResult *tbtc.DKGChainResult, ) error { _, err := tc.walletRegistry.SubmitDkgResult( convertDkgResultToAbiType(dkgResult), ) return err } // computeOperatorsIDsHash computes the keccak256 hash for the given list // of operators IDs. func computeOperatorsIDsHash(operatorsIDs chain.OperatorIDs) ([32]byte, error) { uint32SliceType, err := abi.NewType("uint32[]", "uint32[]", nil) if err != nil { return [32]byte{}, err } bytes, err := abi.Arguments{{Type: uint32SliceType}}.Pack(operatorsIDs) if err != nil { return [32]byte{}, err } return crypto.Keccak256Hash(bytes), nil } // convertSignaturesToChainFormat converts signatures map to two slices. The // first slice contains indices of members from the map, sorted in ascending order // as required by the contract. The second slice is a slice of concatenated // signatures. Signatures and member indices are returned in the matching order. // It requires each signature to be exactly 65-byte long. func convertSignaturesToChainFormat( signatures map[group.MemberIndex][]byte, ) ([]group.MemberIndex, []byte, error) { membersIndexes := make([]group.MemberIndex, 0) for memberIndex := range signatures { membersIndexes = append(membersIndexes, memberIndex) } sort.Slice(membersIndexes, func(i, j int) bool { return membersIndexes[i] < membersIndexes[j] }) signatureSize := 65 var signaturesSlice []byte for _, memberIndex := range membersIndexes { signature := signatures[memberIndex] if len(signature) != signatureSize { return nil, nil, fmt.Errorf( "invalid signature size for member [%v] got [%d] bytes but [%d] bytes required", memberIndex, len(signature), signatureSize, ) } signaturesSlice = append(signaturesSlice, signature...) } return membersIndexes, signaturesSlice, nil } func (tc *TbtcChain) GetDKGState() (tbtc.DKGState, error) { walletCreationState, err := tc.walletRegistry.GetWalletCreationState() if err != nil { return 0, err } var state tbtc.DKGState switch walletCreationState { case 0: state = tbtc.Idle case 1: state = tbtc.AwaitingSeed case 2: state = tbtc.AwaitingResult case 3: state = tbtc.Challenge default: err = fmt.Errorf( "unexpected wallet creation state: [%v]", walletCreationState, ) } return state, err } // CalculateDKGResultSignatureHash calculates a 32-byte hash that is used // to produce a signature supporting the given groupPublicKey computed // as result of the given DKG process. The misbehavedMembersIndexes parameter // should contain indexes of members that were considered as misbehaved // during the DKG process. The startBlock argument is the block at which // the given DKG process started. func (tc *TbtcChain) CalculateDKGResultSignatureHash( groupPublicKey *ecdsa.PublicKey, misbehavedMembersIndexes []group.MemberIndex, startBlock uint64, ) (dkg.ResultSignatureHash, error) { groupPublicKeyBytes := secp256k1.Marshal(groupPublicKey) // Crop the 04 prefix as the calculateDKGResultSignatureHash function // expects an unprefixed 64-byte public key, unprefixedGroupPublicKeyBytes := groupPublicKeyBytes[1:] // Sort misbehavedMembersIndexes slice in ascending order as expected // by the calculateDKGResultSignatureHash function. sort.Slice(misbehavedMembersIndexes[:], func(i, j int) bool { return misbehavedMembersIndexes[i] < misbehavedMembersIndexes[j] }) return calculateDKGResultSignatureHash( tc.chainID, unprefixedGroupPublicKeyBytes, misbehavedMembersIndexes, big.NewInt(int64(startBlock)), ) } // calculateDKGResultSignatureHash computes the keccak256 hash for the given DKG // result parameters. It expects that the groupPublicKey is a 64-byte uncompressed // public key without the 04 prefix and misbehavedMembersIndexes slice is // sorted in ascending order. Those expectations are forced by the contract. func calculateDKGResultSignatureHash( chainID *big.Int, groupPublicKey []byte, misbehavedMembersIndexes []group.MemberIndex, startBlock *big.Int, ) (dkg.ResultSignatureHash, error) { publicKeySize := 64 if len(groupPublicKey) != publicKeySize { return dkg.ResultSignatureHash{}, fmt.Errorf( "wrong group public key length", ) } uint256Type, err := abi.NewType("uint256", "uint256", nil) if err != nil { return dkg.ResultSignatureHash{}, err } bytesType, err := abi.NewType("bytes", "bytes", nil) if err != nil { return dkg.ResultSignatureHash{}, err } uint8SliceType, err := abi.NewType("uint8[]", "uint8[]", nil) if err != nil { return dkg.ResultSignatureHash{}, err } bytes, err := abi.Arguments{ {Type: uint256Type}, {Type: bytesType}, {Type: uint8SliceType}, {Type: uint256Type}, }.Pack( chainID, groupPublicKey, misbehavedMembersIndexes, startBlock, ) if err != nil { return dkg.ResultSignatureHash{}, err } return dkg.ResultSignatureHash(crypto.Keccak256Hash(bytes)), nil } func (tc *TbtcChain) IsDKGResultValid( dkgResult *tbtc.DKGChainResult, ) (bool, error) { outcome, err := tc.walletRegistry.IsDkgResultValid( convertDkgResultToAbiType(dkgResult), ) if err != nil { return false, fmt.Errorf("cannot check result validity: [%v]", err) } return parseDkgResultValidationOutcome(&outcome) } // parseDkgResultValidationOutcome parses the DKG validation outcome and returns // a boolean indicating whether the result is valid or not. The outcome parameter // must be a pointer to a struct containing a boolean flag as the first field. // // TODO: Find a better way to get the validity flag. This would require changes // in the contracts binding generator. // // The nil-pointer, non-struct-element, and zero-field-count guards below are // an intentional improvement added during the #4191 file split; they did not // exist in the pre-split monolithic tbtc.go. They are strict supersets of the // original behavior (the original code would panic on these inputs) and have // no equivalent caller contract that relied on the panic, so callers that // pass well-formed ABI outcomes see no change. func parseDkgResultValidationOutcome( outcome interface{}, ) (bool, error) { value := reflect.ValueOf(outcome) switch value.Kind() { case reflect.Pointer: if value.IsNil() { return false, fmt.Errorf("result validation outcome is nil") } elem := value.Elem() if elem.Kind() != reflect.Struct { return false, fmt.Errorf("result validation outcome is not a struct") } if elem.NumField() == 0 { return false, fmt.Errorf("result validation outcome has no fields") } default: return false, fmt.Errorf("result validation outcome is not a pointer") } field := value.Elem().Field(0) switch field.Kind() { case reflect.Bool: return field.Bool(), nil default: return false, fmt.Errorf("cannot parse result validation outcome") } } func (tc *TbtcChain) ChallengeDKGResult(dkgResult *tbtc.DKGChainResult) error { _, err := tc.walletRegistry.ChallengeDkgResult( convertDkgResultToAbiType(dkgResult), ) return err } func (tc *TbtcChain) ApproveDKGResult(dkgResult *tbtc.DKGChainResult) error { result := convertDkgResultToAbiType(dkgResult) gasEstimate, err := tc.walletRegistry.ApproveDkgResultGasEstimate(result) if err != nil { return err } // The original estimate for this contract call turned out to be too low. gasEstimateWithMargin := gasEstimateWithMargin(gasEstimate) _, err = tc.walletRegistry.ApproveDkgResult( result, ethutil.TransactionOptions{ GasLimit: uint64(gasEstimateWithMargin), }, ) return err } func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) { parameters, err := tc.walletRegistry.DkgParameters() if err != nil { return nil, err } return &tbtc.DKGParameters{ SubmissionTimeoutBlocks: parameters.ResultSubmissionTimeout.Uint64(), ChallengePeriodBlocks: parameters.ResultChallengePeriodLength.Uint64(), ApprovePrecedencePeriodBlocks: parameters.SubmitterPrecedencePeriodLength.Uint64(), }, nil }