diff --git a/evm/evm_client.go b/evm/evm_client.go index 33bee506..70a81814 100644 --- a/evm/evm_client.go +++ b/evm/evm_client.go @@ -216,6 +216,14 @@ func (ec *Client) StateLastEventNonce(opts *bind.CallOpts) (uint64, error) { return nonce.Uint64(), nil } +func (ec *Client) StateLastValidatorSetCheckpoint(opts *bind.CallOpts) ([32]byte, error) { + checkpoint, err := ec.Wrapper.StateLastValidatorSetCheckpoint(opts) + if err != nil { + return [32]byte{}, err + } + return checkpoint, nil +} + func (ec *Client) WaitForTransaction( ctx context.Context, backend bind.DeployBackend, diff --git a/relayer/errors.go b/relayer/errors.go index b21b22e1..6326049d 100644 --- a/relayer/errors.go +++ b/relayer/errors.go @@ -8,4 +8,5 @@ var ( ErrAttestationNotValsetRequest = errors.New("attestation is not a valset request") ErrAttestationNotDataCommitmentRequest = errors.New("attestation is not a data commitment request") ErrAttestationNotFound = errors.New("attestation not found") + ErrValidatorSetMismatch = errors.New("p2p validator set is different from the trusted contract one") ) diff --git a/relayer/relayer.go b/relayer/relayer.go index 798813fd..79c7cd78 100644 --- a/relayer/relayer.go +++ b/relayer/relayer.go @@ -1,7 +1,9 @@ package relayer import ( + "bytes" "context" + "encoding/hex" "fmt" "math/big" "strconv" @@ -144,12 +146,10 @@ func (r *Relayer) ProcessAttestation(ctx context.Context, opts *bind.TransactOpt previousValset, err := r.AppQuerier.QueryLastValsetBeforeNonce(ctx, attI.GetNonce()) if err != nil { r.logger.Error("failed to query the last valset before nonce (probably pruned). recovering via falling back to the P2P network", "err", err.Error()) - latestValset, err := r.P2PQuerier.QueryLatestValset(ctx) + previousValset, err = r.QueryValsetFromP2PNetworkAndValidateIt(ctx) if err != nil { return nil, err } - previousValset = latestValset.ToValset() - r.logger.Info("using the latest valset from P2P network. if the valset is malicious, the Blobstream contract will not accept it", "nonce", previousValset.Nonce) } switch att := attI.(type) { case *celestiatypes.Valset: @@ -194,6 +194,39 @@ func (r *Relayer) ProcessAttestation(ctx context.Context, opts *bind.TransactOpt } } +// QueryValsetFromP2PNetworkAndValidateIt Queries the latest valset from the P2P network +// and validates it against the validator set hash used in the contract. +func (r *Relayer) QueryValsetFromP2PNetworkAndValidateIt(ctx context.Context) (*celestiatypes.Valset, error) { + latestValset, err := r.P2PQuerier.QueryLatestValset(ctx) + if err != nil { + return nil, err + } + vs := latestValset.ToValset() + vsHash, err := vs.SignBytes() + if err != nil { + return nil, err + } + r.logger.Info("found the latest valset in P2P network. Authenticating it against the contract to verify it's valid", "nonce", vs.Nonce, "hash", vsHash.Hex()) + + contractHash, err := r.EVMClient.StateLastValidatorSetCheckpoint(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, err + } + + bzVSHash, err := hex.DecodeString(vsHash.Hex()[2:]) + if err != nil { + return nil, err + } + + if !bytes.Equal(bzVSHash, contractHash[:]) { + r.logger.Error("valset hash from contract mismatches that of P2P one, halting. try running the relayer with an archive node to continue relaying", "contract_vs_hash", ethcmn.Bytes2Hex(contractHash[:]), "p2p_vs_hash", vsHash.Hex()) + return nil, ErrValidatorSetMismatch + } + + r.logger.Info("valset is valid. continuing relaying using the latest valset from P2P network", "nonce", vs.Nonce) + return vs, nil +} + func (r *Relayer) UpdateValidatorSet( ctx context.Context, opts *bind.TransactOpts, diff --git a/relayer/relayer_test.go b/relayer/relayer_test.go index 72701a30..887f1021 100644 --- a/relayer/relayer_test.go +++ b/relayer/relayer_test.go @@ -1,6 +1,7 @@ package relayer_test import ( + "bytes" "context" "math/big" "testing" @@ -118,3 +119,37 @@ func TestUseValsetFromP2P(t *testing.T) { require.NoError(t, err) assert.Equal(t, att.Nonce, lastNonce) } + +func (s *RelayerTestSuite) TestQueryValsetFromP2P() { + t := s.T() + _, err := s.Node.CelestiaNetwork.WaitForHeightWithTimeout(400, 30*time.Second) + require.NoError(t, err) + + ctx := context.Background() + + // process valset nonce so that it is added to the DHT + vs, err := s.Orchestrator.AppQuerier.QueryLatestValset(ctx) + require.NoError(t, err) + err = s.Orchestrator.ProcessValsetEvent(ctx, *vs) + require.NoError(t, err) + + // the valset should be in the DHT + _, err = s.Orchestrator.P2PQuerier.QueryLatestValset(ctx) + require.NoError(t, err) + + // query the valset and authenticate it + p2pVS, err := s.Relayer.QueryValsetFromP2PNetworkAndValidateIt(ctx) + require.NoError(t, err) + + // check if the valset is the same + assert.Equal(t, vs.Nonce, p2pVS.Nonce) + assert.Equal(t, vs.Height, p2pVS.Height) + + // check if the hash is the same + appVSHash, err := vs.Hash() + require.NoError(t, err) + p2pVSHash, err := p2pVS.Hash() + require.NoError(t, err) + + assert.True(t, bytes.Equal(appVSHash.Bytes(), p2pVSHash.Bytes())) +}