diff --git a/contracts/src/BeefyClient.sol b/contracts/src/BeefyClient.sol index fd036939b0..c7a1e6748d 100644 --- a/contracts/src/BeefyClient.sol +++ b/contracts/src/BeefyClient.sol @@ -256,11 +256,12 @@ contract BeefyClient { ValidatorSetState storage vset; uint16 signatureUsageCount; - if (commitment.validatorSetID == currentValidatorSet.id) { + if (commitment.validatorSetID == currentValidatorSet.id || commitment.validatorSetID == nextValidatorSet.id - 1) + { signatureUsageCount = currentValidatorSet.usageCounters.get(proof.index); currentValidatorSet.usageCounters.set(proof.index, signatureUsageCount.saturatingAdd(1)); vset = currentValidatorSet; - } else if (commitment.validatorSetID == nextValidatorSet.id) { + } else if (commitment.validatorSetID >= nextValidatorSet.id) { signatureUsageCount = nextValidatorSet.usageCounters.get(proof.index); nextValidatorSet.usageCounters.set(proof.index, signatureUsageCount.saturatingAdd(1)); vset = nextValidatorSet; @@ -354,11 +355,12 @@ contract BeefyClient { bool is_next_session = false; ValidatorSetState storage vset; - if (commitment.validatorSetID == nextValidatorSet.id) { + if (commitment.validatorSetID == currentValidatorSet.id || commitment.validatorSetID == nextValidatorSet.id - 1) + { + vset = currentValidatorSet; + } else if (commitment.validatorSetID >= nextValidatorSet.id) { is_next_session = true; vset = nextValidatorSet; - } else if (commitment.validatorSetID == currentValidatorSet.id) { - vset = currentValidatorSet; } else { revert InvalidCommitment(); } @@ -368,7 +370,7 @@ contract BeefyClient { bytes32 newMMRRoot = ensureProvidesMMRRoot(commitment); if (is_next_session) { - if (leaf.nextAuthoritySetID != nextValidatorSet.id + 1) { + if (leaf.nextAuthoritySetID <= nextValidatorSet.id) { revert InvalidMMRLeaf(); } bool leafIsValid = diff --git a/contracts/test/BeefyClient.t.sol b/contracts/test/BeefyClient.t.sol index 42566ca4ec..700a324344 100644 --- a/contracts/test/BeefyClient.t.sol +++ b/contracts/test/BeefyClient.t.sol @@ -72,8 +72,6 @@ contract BeefyClientTest is Test { bitSetArray = beefyValidatorSetRaw.readUintArray(".participants"); absentBitSetArray = beefyValidatorSetRaw.readUintArray(".absentees"); - console.log("current validator's merkle root is: %s", Strings.toHexString(uint256(root), 32)); - beefyClient = new BeefyClientMock(randaoCommitDelay, randaoCommitExpiration, minNumRequiredSignatures); bitfield = beefyClient.createInitialBitfield(bitSetArray, setSize); @@ -101,6 +99,20 @@ contract BeefyClientTest is Test { return BeefyClient.Commitment(blockNumber, setId, payload); } + function initializeNonConsecutive(uint32 _setId, uint32 _nextSetId) + public + returns (BeefyClient.Commitment memory) + { + currentSetId = _setId; + nextSetId = _nextSetId; + BeefyClient.ValidatorSet memory vset = BeefyClient.ValidatorSet(currentSetId, setSize, root); + BeefyClient.ValidatorSet memory nextvset = BeefyClient.ValidatorSet(nextSetId, setSize, root); + beefyClient.initialize_public(0, vset, nextvset); + BeefyClient.PayloadItem[] memory payload = new BeefyClient.PayloadItem[](1); + payload[0] = BeefyClient.PayloadItem(mmrRootID, abi.encodePacked(mmrRoot)); + return BeefyClient.Commitment(blockNumber, setId, payload); + } + function printBitArray(uint256[] memory bits) private view { for (uint256 i = 0; i < bits.length; i++) { console.log("bits index at %d is %d", i, bits[i]); @@ -166,7 +178,7 @@ contract BeefyClientTest is Test { ); } - function testSubmit() public returns (BeefyClient.Commitment memory) { + function testSubmitHappyPath() public returns (BeefyClient.Commitment memory) { BeefyClient.Commitment memory commitment = initialize(setId); assertEq(beefyClient.getValidatorCounter(false, finalValidatorProofs[0].index), 0); @@ -388,7 +400,7 @@ contract BeefyClientTest is Test { commitPrevRandao(); } - function testSubmitWithHandover() public { + function testSubmitWithHandoverHappyPath() public { //initialize with previous set BeefyClient.Commitment memory commitment = initialize(setId - 1); @@ -752,4 +764,99 @@ contract BeefyClientTest is Test { BeefyClient.ValidatorSet(nextId, 0, 0x0) ); } + + function testSubmitNonConsecutive() public { + BeefyClient.Commitment memory commitment = initializeNonConsecutive(setId, setId + 2); + + beefyClient.submitInitial(commitment, bitfield, finalValidatorProofs[0]); + + vm.roll(block.number + randaoCommitDelay); + + commitPrevRandao(); + + createFinalProofs(); + + beefyClient.submitFinal( + commitment, bitfield, finalValidatorProofs, emptyLeaf, emptyLeafProofs, emptyLeafProofOrder + ); + + assertEq(beefyClient.latestBeefyBlock(), blockNumber); + + (uint128 _currentSetId,,,) = beefyClient.currentValidatorSet(); + assertEq(_currentSetId, uint128(setId)); + + (uint128 _nextSetId,,,) = beefyClient.nextValidatorSet(); + assertEq(_nextSetId, uint128(setId + 2)); + } + + function testSubmitNonConsecutiveCommitNotInCurrentSet() public { + BeefyClient.Commitment memory commitment = initializeNonConsecutive(setId - 1, setId + 1); + + beefyClient.submitInitial(commitment, bitfield, finalValidatorProofs[0]); + + vm.roll(block.number + randaoCommitDelay); + + commitPrevRandao(); + + createFinalProofs(); + + beefyClient.submitFinal(commitment, bitfield, finalValidatorProofs, mmrLeaf, mmrLeafProofs, leafProofOrder); + assertEq(beefyClient.latestBeefyBlock(), blockNumber); + + (uint128 _currentSetId,,,) = beefyClient.currentValidatorSet(); + assertEq(_currentSetId, uint128(setId - 1)); + + (uint128 _nextSetId,,,) = beefyClient.nextValidatorSet(); + assertEq(_nextSetId, uint128(setId + 1)); + } + + function testSubmitWithHandoverNonConsecutive() public { + BeefyClient.Commitment memory commitment = initializeNonConsecutive(setId - 2, setId); + + beefyClient.submitInitial(commitment, bitfield, finalValidatorProofs[0]); + + vm.roll(block.number + randaoCommitDelay); + + commitPrevRandao(); + + createFinalProofs(); + + beefyClient.submitFinal(commitment, bitfield, finalValidatorProofs, mmrLeaf, mmrLeafProofs, leafProofOrder); + assertEq(beefyClient.latestBeefyBlock(), blockNumber); + + (uint128 _currentSetId,,,) = beefyClient.currentValidatorSet(); + assertEq(_currentSetId, uint128(setId)); + + (uint128 _nextSetId,,,) = beefyClient.nextValidatorSet(); + assertEq(_nextSetId, uint128(setId + 1)); + } + + function testSubmitWithHandoverNonConsecutiveCommitmentMoreThanNextSetID() public { + BeefyClient.Commitment memory commitment = initializeNonConsecutive(setId - 2, setId - 1); + + beefyClient.submitInitial(commitment, bitfield, finalValidatorProofs[0]); + + vm.roll(block.number + randaoCommitDelay); + + commitPrevRandao(); + + createFinalProofs(); + + beefyClient.submitFinal(commitment, bitfield, finalValidatorProofs, mmrLeaf, mmrLeafProofs, leafProofOrder); + assertEq(beefyClient.latestBeefyBlock(), blockNumber); + + (uint128 _currentSetId,,,) = beefyClient.currentValidatorSet(); + assertEq(_currentSetId, uint128(setId - 1)); + + (uint128 _nextSetId,,,) = beefyClient.nextValidatorSet(); + assertEq(_nextSetId, uint128(setId + 1)); + } + + function testSubmitNonConsecutiveCommitInvalidSetId() public { + BeefyClient.Commitment memory commitment = initializeNonConsecutive(setId - 1, setId + 2); + + vm.expectRevert(BeefyClient.InvalidCommitment.selector); + + beefyClient.submitInitial(commitment, bitfield, finalValidatorProofs[0]); + } } diff --git a/contracts/test/mocks/BeefyClientMock.sol b/contracts/test/mocks/BeefyClientMock.sol index 40caee279b..f28a3a454f 100644 --- a/contracts/test/mocks/BeefyClientMock.sol +++ b/contracts/test/mocks/BeefyClientMock.sol @@ -43,7 +43,6 @@ contract BeefyClientMock is BeefyClient { nextValidatorSet.length = _nextValidatorSet.length; nextValidatorSet.root = _nextValidatorSet.root; nextValidatorSet.usageCounters = createUint16Array(nextValidatorSet.length); - console.log(currentValidatorSet.usageCounters.data.length); } // Used to verify integrity of storage to storage copies diff --git a/relayer/chain/relaychain/connection.go b/relayer/chain/relaychain/connection.go index c2d1e38124..87407d7f94 100644 --- a/relayer/chain/relaychain/connection.go +++ b/relayer/chain/relaychain/connection.go @@ -6,6 +6,7 @@ package relaychain import ( "context" "fmt" + "strings" gsrpc "github.com/snowfork/go-substrate-rpc-client/v4" "github.com/snowfork/go-substrate-rpc-client/v4/types" @@ -18,6 +19,7 @@ type Connection struct { api *gsrpc.SubstrateAPI metadata types.Metadata genesisHash types.Hash + isRococo bool } func NewConnection(endpoint string) *Connection { @@ -34,6 +36,10 @@ func (co *Connection) Metadata() *types.Metadata { return &co.metadata } +func (co *Connection) IsRococo() bool { + return co.isRococo +} + func (co *Connection) Connect(_ context.Context) error { // Initialize API api, err := gsrpc.NewSubstrateAPI(co.endpoint) @@ -56,6 +62,15 @@ func (co *Connection) Connect(_ context.Context) error { } co.genesisHash = genesisHash + // Fetch chain name + chainName, err := api.RPC.System.Chain() + if err != nil { + return err + } + if strings.HasPrefix(string(chainName), "Rococo") { + co.isRococo = true + } + log.WithFields(log.Fields{ "endpoint": co.endpoint, "metaVersion": meta.Version, diff --git a/relayer/cmd/scan_beefy.go b/relayer/cmd/scan_beefy.go index 5c54550739..ba711eba11 100644 --- a/relayer/cmd/scan_beefy.go +++ b/relayer/cmd/scan_beefy.go @@ -60,7 +60,11 @@ func ScanBeefyFn(cmd *cobra.Command, _ []string) error { "validator-set-id": validatorSetID, }).Info("Connected to relaychain.") - commitments, err := polkadotListener.Start(ctx, eg, beefyBlock, validatorSetID) + var currentState beefy.BeefyClientState + currentState.CurrentValidatorSetID = validatorSetID + currentState.LatestBeefyBlock = beefyBlock + + commitments, err := polkadotListener.Start(ctx, eg, ¤tState) if err != nil { logrus.WithError(err).Fatalf("could not start") } diff --git a/relayer/relays/beefy/ethereum-writer.go b/relayer/relays/beefy/ethereum-writer.go index 187c190f97..d73b3260b2 100644 --- a/relayer/relays/beefy/ethereum-writer.go +++ b/relayer/relays/beefy/ethereum-writer.go @@ -67,6 +67,15 @@ func (wr *EthereumWriter) Start(ctx context.Context, eg *errgroup.Group, request // Mandatory commitments are always signed by the next validator set recorded in // the beefy light client task.ValidatorsRoot = state.NextValidatorSetRoot + if task.ValidatorsRoot == task.NextValidatorsRoot { + log.WithFields(logrus.Fields{ + "beefyBlockNumber": task.SignedCommitment.Commitment.BlockNumber, + "latestBeefyBlock": state.LatestBeefyBlock, + "validatorSetId": task.SignedCommitment.Commitment.ValidatorSetID, + "nextValidatorSetId": state.NextValidatorSetID, + }).Info("beefy authorities not changed") + continue + } err = wr.submit(ctx, task) if err != nil { @@ -96,7 +105,6 @@ func (wr *EthereumWriter) queryBeefyClientState(ctx context.Context) (*BeefyClie if err != nil { return nil, err } - currentValidatorSet, err := wr.contract.CurrentValidatorSet(&callOpts) if err != nil { return nil, err diff --git a/relayer/relays/beefy/main.go b/relayer/relays/beefy/main.go index 209e33e5d8..93c743d4a8 100644 --- a/relayer/relays/beefy/main.go +++ b/relayer/relays/beefy/main.go @@ -67,7 +67,7 @@ func (relay *Relay) Start(ctx context.Context, eg *errgroup.Group) error { "validatorSetID": initialState.CurrentValidatorSetID, }).Info("Retrieved current BeefyClient state") - requests, err := relay.polkadotListener.Start(ctx, eg, initialState.LatestBeefyBlock, initialState.CurrentValidatorSetID) + requests, err := relay.polkadotListener.Start(ctx, eg, initialState) if err != nil { return fmt.Errorf("initialize polkadot listener: %w", err) } @@ -128,17 +128,19 @@ func (relay *Relay) OneShotSync(ctx context.Context, blockNumber uint64) error { return nil } if task.SignedCommitment.Commitment.ValidatorSetID > state.NextValidatorSetID { - log.WithFields(log.Fields{ - "latestBeefyBlock": state.LatestBeefyBlock, - "currentValidatorSetID": state.CurrentValidatorSetID, - "nextValidatorSetID": state.NextValidatorSetID, - "validatorSetIDToSync": task.SignedCommitment.Commitment.ValidatorSetID, - }).Warn("Task unexpected, wait for mandatory updates to catch up first") - return nil + if task.NextValidatorsRoot != state.NextValidatorSetRoot { + log.WithFields(log.Fields{ + "latestBeefyBlock": state.LatestBeefyBlock, + "currentValidatorSetID": state.CurrentValidatorSetID, + "nextValidatorSetID": state.NextValidatorSetID, + "validatorSetIDToSync": task.SignedCommitment.Commitment.ValidatorSetID, + }).Warn("Task unexpected, wait for mandatory updates to catch up first") + return nil + } } // Submit the task - if task.SignedCommitment.Commitment.ValidatorSetID == state.CurrentValidatorSetID { + if task.SignedCommitment.Commitment.ValidatorSetID == state.CurrentValidatorSetID || task.SignedCommitment.Commitment.ValidatorSetID == state.NextValidatorSetID-1 { task.ValidatorsRoot = state.CurrentValidatorSetRoot } else { task.ValidatorsRoot = state.NextValidatorSetRoot diff --git a/relayer/relays/beefy/polkadot-listener.go b/relayer/relays/beefy/polkadot-listener.go index 24894af1e8..8370e14945 100644 --- a/relayer/relays/beefy/polkadot-listener.go +++ b/relayer/relays/beefy/polkadot-listener.go @@ -31,14 +31,13 @@ func NewPolkadotListener( func (li *PolkadotListener) Start( ctx context.Context, eg *errgroup.Group, - currentBeefyBlock uint64, - currentValidatorSetID uint64, + currentState *BeefyClientState, ) (<-chan Request, error) { requests := make(chan Request, 1) eg.Go(func() error { defer close(requests) - err := li.scanCommitments(ctx, currentBeefyBlock, currentValidatorSetID, requests) + err := li.scanCommitments(ctx, currentState, requests) if err != nil { return err } @@ -50,15 +49,13 @@ func (li *PolkadotListener) Start( func (li *PolkadotListener) scanCommitments( ctx context.Context, - currentBeefyBlock uint64, - currentValidatorSet uint64, + currentState *BeefyClientState, requests chan<- Request, ) error { - in, err := ScanCommitments(ctx, li.conn.Metadata(), li.conn.API(), currentBeefyBlock+1) + in, err := ScanCommitments(ctx, li.conn.Metadata(), li.conn.API(), currentState.LatestBeefyBlock+1) if err != nil { return fmt.Errorf("scan provable commitments: %w", err) } - for { select { case <-ctx.Done(): @@ -79,11 +76,15 @@ func (li *PolkadotListener) scanCommitments( if err != nil { return fmt.Errorf("fetch beefy authorities at block %v: %w", result.BlockHash, err) } - + nextAuthoritySet, err := li.queryBeefyNextAuthoritySet(result.BlockHash) + if err != nil { + return fmt.Errorf("fetch beefy authorities set at block %v: %w", result.BlockHash, err) + } task := Request{ - Validators: validators, - SignedCommitment: result.SignedCommitment, - Proof: result.Proof, + Validators: validators, + SignedCommitment: result.SignedCommitment, + Proof: result.Proof, + NextValidatorsRoot: nextAuthoritySet.Root, } log.WithFields(log.Fields{ @@ -92,7 +93,7 @@ func (li *PolkadotListener) scanCommitments( "validatorSetID": validatorSetID, "nextValidatorSetID": nextValidatorSetID, }, - "validatorSetID": currentValidatorSet, + "validatorSetID": currentState.CurrentValidatorSetID, }).Info("Sending BEEFY commitment to ethereum writer") select { @@ -121,6 +122,26 @@ func (li *PolkadotListener) queryBeefyAuthorities(blockHash types.Hash) ([]subst return authorities, nil } +func (li *PolkadotListener) queryBeefyNextAuthoritySet(blockHash types.Hash) (types.BeefyNextAuthoritySet, error) { + var nextAuthoritySet types.BeefyNextAuthoritySet + var storageKey types.StorageKey + var err error + if li.conn.IsRococo() { + storageKey, err = types.CreateStorageKey(li.conn.Metadata(), "MmrLeaf", "BeefyNextAuthorities", nil, nil) + } else { + storageKey, err = types.CreateStorageKey(li.conn.Metadata(), "BeefyMmrLeaf", "BeefyNextAuthorities", nil, nil) + } + ok, err := li.conn.API().RPC.State.GetStorage(storageKey, &nextAuthoritySet, blockHash) + if err != nil { + return nextAuthoritySet, err + } + if !ok { + return nextAuthoritySet, fmt.Errorf("beefy nextAuthoritySet not found") + } + + return nextAuthoritySet, nil +} + func (li *PolkadotListener) generateBeefyUpdate(relayBlockNumber uint64) (Request, error) { api := li.conn.API() meta := li.conn.Metadata() @@ -142,10 +163,15 @@ func (li *PolkadotListener) generateBeefyUpdate(relayBlockNumber uint64) (Reques if err != nil { return request, fmt.Errorf("fetch beefy authorities at block %v: %w", committedBeefyBlockHash, err) } + nextAuthoritySet, err := li.queryBeefyNextAuthoritySet(committedBeefyBlockHash) + if err != nil { + return request, fmt.Errorf("fetch beefy authorities set at block %v: %w", committedBeefyBlockHash, err) + } request = Request{ - Validators: validators, - SignedCommitment: *commitment, - Proof: *proof, + Validators: validators, + SignedCommitment: *commitment, + Proof: *proof, + NextValidatorsRoot: nextAuthoritySet.Root, } return request, nil @@ -170,8 +196,8 @@ func (li *PolkadotListener) findNextBeefyBlock(blockNumber uint64) (types.Hash, // The relay block not finalized yet, just wait and retry time.Sleep(6 * time.Second) continue - } else if latestBeefyBlockNumber <= nextBeefyBlockNumber+600 { - // The relay block has been finalized not long ago(1 hour), just return the finalized block + } else if latestBeefyBlockNumber <= nextBeefyBlockNumber+60 { + // The relay block has been finalized not long ago, just return the finalized block nextBeefyBlockHash = finalizedBeefyBlockHash break } else { diff --git a/relayer/relays/beefy/task.go b/relayer/relays/beefy/task.go index e35862f40f..3a8f2436f7 100644 --- a/relayer/relays/beefy/task.go +++ b/relayer/relays/beefy/task.go @@ -17,8 +17,9 @@ type BeefyAuthoritySet struct { type Request struct { // Validators that signed this commitment - Validators []substrate.Authority - ValidatorsRoot [32]byte - SignedCommitment types.SignedCommitment - Proof merkle.SimplifiedMMRProof + Validators []substrate.Authority + ValidatorsRoot [32]byte + SignedCommitment types.SignedCommitment + Proof merkle.SimplifiedMMRProof + NextValidatorsRoot [32]byte }