diff --git a/pkg/chain/ethereum/ethereum.go b/pkg/chain/ethereum/ethereum.go index 549b7ffa5c..c641a545ae 100644 --- a/pkg/chain/ethereum/ethereum.go +++ b/pkg/chain/ethereum/ethereum.go @@ -414,6 +414,19 @@ func (bc *baseChain) GetBlockNumberByTimestamp( return block.NumberU64(), nil } +// GetBlockHashByNumber gets the block hash for the given block number. +func (bc *baseChain) GetBlockHashByNumber(blockNumber uint64) ( + [32]byte, + error, +) { + block, err := bc.blockByNumber(blockNumber) + if err != nil { + return [32]byte{}, fmt.Errorf("cannot get block: [%v]", err) + } + + return block.Hash(), nil +} + // currentBlock fetches the current block. func (bc *baseChain) currentBlock() (*types.Block, error) { currentBlockNumber, err := bc.blockCounter.CurrentBlock() diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index f6aa241b1c..fe2240b0c4 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -452,6 +452,8 @@ type Chain interface { // If the aforementioned is not possible, it tries to return the closest // possible block. GetBlockNumberByTimestamp(timestamp uint64) (uint64, error) + // GetBlockHashByNumber gets the block hash for the given block number. + GetBlockHashByNumber(blockNumber uint64) ([32]byte, error) sortition.Chain GroupSelectionChain diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 45d60b830c..eaad15c787 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -50,6 +50,9 @@ type localChain struct { blocksByTimestampMutex sync.Mutex blocksByTimestamp map[uint64]uint64 + blocksHashesByNumberMutex sync.Mutex + blocksHashesByNumber map[uint64][32]byte + pastDepositRevealedEventsMutex sync.Mutex pastDepositRevealedEvents map[[32]byte][]*DepositRevealedEvent @@ -105,6 +108,39 @@ func (lc *localChain) setBlockNumberByTimestamp(timestamp uint64, block uint64) lc.blocksByTimestamp[timestamp] = block } +func (lc *localChain) GetBlockHashByNumber(blockNumber uint64) ( + [32]byte, + error, +) { + lc.blocksHashesByNumberMutex.Lock() + defer lc.blocksHashesByNumberMutex.Unlock() + + blockHash, ok := lc.blocksHashesByNumber[blockNumber] + if !ok { + return [32]byte{}, fmt.Errorf("block not found") + } + + return blockHash, nil +} + +func (lc *localChain) setBlockHashByNumber( + blockNumber uint64, + blockHashString string, +) { + lc.blocksHashesByNumberMutex.Lock() + defer lc.blocksHashesByNumberMutex.Unlock() + + blockHashBytes, err := hex.DecodeString(blockHashString) + if err != nil { + panic(err) + } + + var blockHash [32]byte + copy(blockHash[:], blockHashBytes) + + lc.blocksHashesByNumber[blockNumber] = blockHash +} + func (lc *localChain) OperatorToStakingProvider() (chain.Address, bool, error) { panic("unsupported") } @@ -823,6 +859,7 @@ func ConnectWithKey( ), wallets: make(map[[20]byte]*WalletChainData), blocksByTimestamp: make(map[uint64]uint64), + blocksHashesByNumber: make(map[uint64][32]byte), pastDepositRevealedEvents: make(map[[32]byte][]*DepositRevealedEvent), depositSweepProposalValidations: make(map[[32]byte]bool), pendingRedemptionRequests: make(map[[32]byte]*RedemptionRequest), diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 4c1859499e..bc5e9323b1 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -2,7 +2,9 @@ package tbtc import ( "context" + "crypto/sha256" "fmt" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" @@ -29,6 +31,11 @@ const ( // coordination window. coordinationDurationBlocks = coordinationActivePhaseDurationBlocks + coordinationPassivePhaseDurationBlocks + // coordinationSafeBlockShift is the number of blocks by which the + // coordination block is shifted to obtain a safe block whose 32-byte + // hash can be used as an ingredient for the coordination seed, computed + // for the given coordination window. + coordinationSafeBlockShift = 32 ) // errCoordinationExecutorBusy is an error returned when the coordination @@ -138,9 +145,7 @@ func (cft CoordinationFaultType) String() string { // coordinationFault represents a single coordination fault. type coordinationFault struct { - // culprit is the address of the operator that is responsible for the fault. - culprit chain.Address - // faultType is the type of the fault. + culprit chain.Address // address of the operator responsible for the fault faultType CoordinationFaultType } @@ -200,7 +205,11 @@ func (cr *coordinationResult) String() string { type coordinationExecutor struct { lock *semaphore.Weighted - signers []*signer // TODO: Do we need whole signers? + chain Chain + + coordinatedWallet wallet + membersIndexes []group.MemberIndex + broadcastChannel net.BroadcastChannel membershipValidator *group.MembershipValidator protocolLatch *generator.ProtocolLatch @@ -209,29 +218,34 @@ type coordinationExecutor struct { // newCoordinationExecutor creates a new coordination executor for the // given wallet. func newCoordinationExecutor( - signers []*signer, + chain Chain, + coordinatedWallet wallet, + membersIndexes []group.MemberIndex, broadcastChannel net.BroadcastChannel, membershipValidator *group.MembershipValidator, protocolLatch *generator.ProtocolLatch, ) *coordinationExecutor { return &coordinationExecutor{ lock: semaphore.NewWeighted(1), - signers: signers, + chain: chain, + coordinatedWallet: coordinatedWallet, + membersIndexes: membersIndexes, broadcastChannel: broadcastChannel, membershipValidator: membershipValidator, protocolLatch: protocolLatch, } } -// wallet returns the wallet this executor is responsible for. -func (ce *coordinationExecutor) wallet() wallet { - // All signers belong to one wallet. Take that wallet from the - // first signer. - return ce.signers[0].wallet +// walletPublicKeyHash returns the 20-byte public key hash of the +// coordinated wallet. +func (ce *coordinationExecutor) walletPublicKeyHash() [20]byte { + return bitcoin.PublicKeyHash(ce.coordinatedWallet.publicKey) } // coordinate executes the coordination procedure for the given coordination // window. +// +// TODO: Add logging. func (ce *coordinationExecutor) coordinate( window *coordinationWindow, ) (*coordinationResult, error) { @@ -240,18 +254,48 @@ func (ce *coordinationExecutor) coordinate( } defer ce.lock.Release(1) - // TODO: Implement coordination logic. Remember about: - // - Setting up the right context - // - Using the protocol latch - // - Using the membership validator - // Example result: + ce.protocolLatch.Lock() + defer ce.protocolLatch.Unlock() + + coordinationSeed, err := ce.coordinationSeed(window) + if err != nil { + return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err) + } + + fmt.Printf("coordinationSeed: %v\n", coordinationSeed) + + // TODO: Implement the rest of the coordination procedure. result := &coordinationResult{ - wallet: ce.wallet(), + wallet: ce.coordinatedWallet, window: window, - leader: ce.wallet().signingGroupOperators[0], + leader: ce.coordinatedWallet.signingGroupOperators[0], proposal: &noopProposal{}, faults: nil, } return result, nil } + +// coordinationSeed computes the coordination seed for the given coordination +// window. +func (ce *coordinationExecutor) coordinationSeed( + window *coordinationWindow, +) ([32]byte, error) { + walletPublicKeyHash := ce.walletPublicKeyHash() + + safeBlockNumber := window.coordinationBlock - coordinationSafeBlockShift + safeBlockHash, err := ce.chain.GetBlockHashByNumber(safeBlockNumber) + if err != nil { + return [32]byte{}, fmt.Errorf( + "failed to get safe block hash: [%v]", + err, + ) + } + + return sha256.Sum256( + append( + walletPublicKeyHash[:], + safeBlockHash[:]..., + ), + ), nil +} diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index e6cfcc4c1b..3e99dd9db2 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -2,6 +2,7 @@ package tbtc import ( "context" + "encoding/hex" "testing" "time" @@ -116,3 +117,49 @@ func TestWatchCoordinationWindows(t *testing.T) { int(receivedWindows[1].coordinationBlock), ) } + +func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { + window := newCoordinationWindow(900) + + localChain := Connect() + + localChain.setBlockHashByNumber( + window.coordinationBlock-32, + "1322996cbcbc38fc924a46f4df5f9064279d3ab43396e58386dac9b87440d64f", + ) + + // Uncompressed public key corresponding to the 20-byte public key hash: + // aa768412ceed10bd423c025542ca90071f9fb62d. + publicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + coordinatedWallet := wallet{ + // Set only relevant fields. + publicKey: unmarshalPublicKey(publicKeyHex), + } + + executor := &coordinationExecutor{ + chain: localChain, + coordinatedWallet: coordinatedWallet, + } + + seed, err := executor.coordinationSeed(window) + if err != nil { + t.Fatal(err) + } + + // Expected seed is sha256(wallet_public_key_hash | safe_block_hash). + expectedSeed := "e55c779d6d83183409ddc90c6cd5130567f0593349a9c82494b402048ec2d03d" + + testutils.AssertStringsEqual( + t, + "coordination seed", + expectedSeed, + hex.EncodeToString(seed[:]), + ) +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 48303a62ca..9c0650af38 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -370,8 +370,17 @@ func (n *node) getCoordinationExecutor( len(signers), ) + // The coordination executor does not need access to signers' key material. + // It is enough to pass only their member indexes. + membersIndexes := make([]group.MemberIndex, len(signers)) + for i, s := range signers { + membersIndexes[i] = s.signingGroupMemberIndex + } + executor := newCoordinationExecutor( - signers, + n.chain, + wallet, + membersIndexes, broadcastChannel, membershipValidator, n.protocolLatch, diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index 5fc3ca15d0..200789dff5 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -198,10 +198,13 @@ func TestNode_GetCoordinationExecutor(t *testing.T) { t, "signers count", 1, - len(executor.signers), + len(executor.membersIndexes), ) - if !reflect.DeepEqual(signer, executor.signers[0]) { + if !reflect.DeepEqual( + signer.signingGroupMemberIndex, + executor.membersIndexes[0], + ) { t.Errorf("executor holds an unexpected signer") }