diff --git a/aggsender/aggsender.go b/aggsender/aggsender.go index 4499a110c..fb8a4dd91 100644 --- a/aggsender/aggsender.go +++ b/aggsender/aggsender.go @@ -96,6 +96,7 @@ func New( l1InfoTreeSyncer, l2Syncer, rollupDataQuerier, + committeeQuerier, ) if err != nil { return nil, fmt.Errorf("error creating flow manager: %w", err) diff --git a/aggsender/aggsender_test.go b/aggsender/aggsender_test.go index 67c894e75..ff3a0df76 100644 --- a/aggsender/aggsender_test.go +++ b/aggsender/aggsender_test.go @@ -76,6 +76,7 @@ func TestAggSenderStart(t *testing.T) { epochNotifierMock := mocks.NewEpochNotifier(t) bridgeL2SyncerMock := mocks.NewL2BridgeSyncer(t) rollupQuerierMock := mocks.NewRollupDataQuerier(t) + committeQuerierMock := mocks.NewMultisigQuerier(t) ch := make(chan aggsendertypes.EpochEvent) epochNotifierMock.EXPECT().Subscribe("aggsender").Return(ch) epochNotifierMock.EXPECT().GetEpochStatus().Return(aggsendertypes.EpochStatus{}).Once() @@ -84,6 +85,9 @@ func TestAggSenderStart(t *testing.T) { aggLayerMock.EXPECT().GetLatestPendingCertificateHeader(mock.Anything, mock.Anything).Return(nil, nil) aggLayerMock.EXPECT().GetLatestSettledCertificateHeader(mock.Anything, mock.Anything).Return(nil, nil) rollupQuerierMock.EXPECT().GetRollupChainID().Return(uint64(1234), nil) + committee, err := aggsendertypes.NewMultisigCommittee([]*aggsendertypes.SignerInfo{aggsendertypes.NewSignerInfo("", common.Address{})}, 1) + require.NoError(t, err) + committeQuerierMock.EXPECT().GetMultisigCommittee(mock.Anything, mock.Anything).Return(committee, nil).Once() ctx := t.Context() aggSender, err := New( @@ -104,7 +108,7 @@ func TestAggSenderStart(t *testing.T) { nil, // l1 client nil, // l2 client rollupQuerierMock, - nil, // committee querier + committeQuerierMock, ) require.NoError(t, err) require.NotNil(t, aggSender) @@ -570,8 +574,13 @@ func TestGetValidators(t *testing.T) { func TestNewAggSender(t *testing.T) { mockBridgeSyncer := mocks.NewL2BridgeSyncer(t) mockRollupQuerier := mocks.NewRollupDataQuerier(t) + mockCommitteeQuerier := mocks.NewMultisigQuerier(t) mockBridgeSyncer.EXPECT().OriginNetwork().Return(uint32(1)).Times(2) mockRollupQuerier.EXPECT().GetRollupChainID().Return(uint64(1234), nil) + committee, err := aggsendertypes.NewMultisigCommittee([]*aggsendertypes.SignerInfo{aggsendertypes.NewSignerInfo("", common.Address{})}, 1) + require.NoError(t, err) + mockCommitteeQuerier.EXPECT().GetMultisigCommittee(mock.Anything, mock.Anything).Return(committee, nil).Once() + sut, err := New(context.TODO(), log.WithFields("module", "ut"), config.Config{ AggsenderPrivateKey: signertypes.SignerConfig{ Method: signertypes.MethodNone, @@ -582,7 +591,7 @@ func TestNewAggSender(t *testing.T) { nil, // l1 client nil, // l2 client mockRollupQuerier, - nil, // committee querier + mockCommitteeQuerier, ) require.NoError(t, err) require.NotNil(t, sut) diff --git a/aggsender/config/config.go b/aggsender/config/config.go index cf0931083..e366d4b68 100644 --- a/aggsender/config/config.go +++ b/aggsender/config/config.go @@ -87,6 +87,8 @@ type Config struct { ValidatorClient *grpc.ClientConfig `mapstructure:"ValidatorClient"` // RetriesToBuildAndSendCertificate is the configuration for the retries to build and send a certificate RetriesToBuildAndSendCertificate common.RetryPolicyGenericConfig `mapstructure:"RetriesToBuildAndSendCertificate"` + // RequireCommitteeMembershipCheck indicates whether to check if the signer is part of the committee + RequireCommitteeMembershipCheck bool `mapstructure:"RequireCommitteeMembershipCheck"` } func (c Config) CheckCertConfigBriefString() string { diff --git a/aggsender/flows/factory.go b/aggsender/flows/factory.go index 453facfac..dfe017104 100644 --- a/aggsender/flows/factory.go +++ b/aggsender/flows/factory.go @@ -3,6 +3,7 @@ package flows import ( "context" "fmt" + "math/big" "time" "github.com/agglayer/aggkit/aggsender/aggchainproofclient" @@ -34,14 +35,18 @@ func NewFlow( l1InfoTreeSyncer types.L1InfoTreeSyncer, l2Syncer types.L2BridgeSyncer, rollupDataQuerier types.RollupDataQuerier, + committeeQuerier types.MultisigQuerier, ) (types.AggsenderFlow, error) { switch types.AggsenderMode(cfg.Mode) { case types.PessimisticProofMode: commonFlowComponents, err := CreateCommonFlowComponents( - ctx, logger, storage, l1Client, l1InfoTreeSyncer, l2Syncer, rollupDataQuerier, 0, false, + ctx, logger, storage, l1Client, l1InfoTreeSyncer, l2Syncer, + rollupDataQuerier, committeeQuerier, + 0, false, cfg.MaxCertSize, cfg.RollupCreationBlockL1, cfg.DelayBetweenRetries.Duration, cfg.AggsenderPrivateKey, true, + cfg.RequireCommitteeMembershipCheck, ) if err != nil { return nil, fmt.Errorf("failed to create common flow components: %w", err) @@ -81,11 +86,13 @@ func NewFlow( } commonFlowComponents, err := CreateCommonFlowComponents( - ctx, logger, storage, l1Client, l1InfoTreeSyncer, l2Syncer, rollupDataQuerier, + ctx, logger, storage, l1Client, l1InfoTreeSyncer, l2Syncer, + rollupDataQuerier, committeeQuerier, aggchainFEPQuerier.StartL2Block(), cfg.RequireNoFEPBlockGap, cfg.MaxCertSize, cfg.RollupCreationBlockL1, cfg.DelayBetweenRetries.Duration, cfg.AggsenderPrivateKey, true, + cfg.RequireCommitteeMembershipCheck, ) if err != nil { return nil, fmt.Errorf("failed to create common flow components: %w", err) @@ -139,6 +146,7 @@ func CreateCommonFlowComponents( l1InfoTreeSyncer types.L1InfoTreeSyncer, l2Syncer types.L2BridgeSyncer, rollupDataQuerier types.RollupDataQuerier, + committeeQuerier types.MultisigQuerier, startL2Block uint64, requireNoFEPBlockGap bool, maxCertSize uint, @@ -146,13 +154,15 @@ func CreateCommonFlowComponents( delayBetweenRetries time.Duration, signerCfg signertypes.SignerConfig, fullClaimsRequired bool, + requireCommitteeMembershipCheck bool, ) (*CommonFlowComponents, error) { l2ChainID, err := rollupDataQuerier.GetRollupChainID() if err != nil { return nil, fmt.Errorf("error getting rollup chain id: %w", err) } - signer, err := initializeSigner(ctx, signerCfg, logger, l2ChainID) + signer, err := initializeSigner(ctx, signerCfg, logger, l2ChainID, + committeeQuerier, requireCommitteeMembershipCheck) if err != nil { return nil, err } @@ -180,6 +190,8 @@ func initializeSigner( signerCfg signertypes.SignerConfig, logger *log.Logger, l2ChainID uint64, + committeeQuerier types.MultisigQuerier, + requireCommitteeMembershipCheck bool, ) (signertypes.Signer, error) { signer, err := signer.NewSigner(ctx, l2ChainID, signerCfg, aggkitcommon.AGGSENDER, logger) if err != nil { @@ -190,5 +202,25 @@ func initializeSigner( return nil, fmt.Errorf("error signer.Initialize. Err: %w", err) } + multisigCommittee, err := committeeQuerier.GetMultisigCommittee(ctx, big.NewInt(int64(aggkittypes.Latest))) + if err != nil { + if requireCommitteeMembershipCheck { + return nil, fmt.Errorf("error getting multisig committee: %w", err) + } + + logger.Warnf("error getting multisig committee: %v", err) + return signer, nil + } + + if !multisigCommittee.IsMember(signer.PublicAddress()) { + if requireCommitteeMembershipCheck { + return nil, fmt.Errorf("signer address %s is not part of the multisig committee: %s", + signer.PublicAddress(), multisigCommittee.String()) + } + + logger.Warnf("signer address %s is not part of the multisig committee: %s", + signer.PublicAddress(), multisigCommittee.String()) + } + return signer, nil } diff --git a/aggsender/flows/factory_test.go b/aggsender/flows/factory_test.go index 1553bed91..27da71768 100644 --- a/aggsender/flows/factory_test.go +++ b/aggsender/flows/factory_test.go @@ -2,6 +2,7 @@ package flows import ( "context" + "errors" "testing" "time" @@ -14,6 +15,7 @@ import ( "github.com/agglayer/aggkit/log" typesmocks "github.com/agglayer/aggkit/types/mocks" signertypes "github.com/agglayer/go_signer/signer/types" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -26,6 +28,7 @@ func TestNewFlow(t *testing.T) { testCases := []struct { name string cfg config.Config + mockFn func(*mocks.MultisigQuerier) expectedError string }{ { @@ -36,6 +39,82 @@ func TestNewFlow(t *testing.T) { MaxCertSize: 100, AggkitProverClient: aggkitgrpc.DefaultConfig(), }, + mockFn: func(mockCommittee *mocks.MultisigQuerier) { + committee, err := types.NewMultisigCommittee([]*types.SignerInfo{types.NewSignerInfo("", common.Address{})}, 1) + require.NoError(t, err) + + mockCommittee.EXPECT().GetMultisigCommittee(mock.Anything, mock.Anything).Return(committee, nil).Maybe() + }, + }, + { + name: "error getting multisig committee when RequireCommitteeMembershipCheck is true", + cfg: config.Config{ + Mode: string(types.PessimisticProofMode), + AggsenderPrivateKey: signertypes.SignerConfig{Method: signertypes.MethodNone}, + MaxCertSize: 100, + AggkitProverClient: aggkitgrpc.DefaultConfig(), + RequireCommitteeMembershipCheck: true, + }, + mockFn: func(mockCommittee *mocks.MultisigQuerier) { + mockCommittee.EXPECT().GetMultisigCommittee(mock.Anything, mock.Anything).Return(nil, errors.New("test error")).Maybe() + }, + expectedError: "error getting multisig committee: test error", + }, + { + name: "error getting multisig committee when RequireCommitteeMembershipCheck is false", + cfg: config.Config{ + Mode: string(types.PessimisticProofMode), + AggsenderPrivateKey: signertypes.SignerConfig{Method: signertypes.MethodNone}, + MaxCertSize: 100, + AggkitProverClient: aggkitgrpc.DefaultConfig(), + RequireCommitteeMembershipCheck: false, + }, + mockFn: func(mockCommittee *mocks.MultisigQuerier) { + mockCommittee.EXPECT().GetMultisigCommittee(mock.Anything, mock.Anything).Return(nil, errors.New("test error")).Maybe() + }, + }, + { + name: "committee membership check disabled with PessimisticProofMode", + cfg: config.Config{ + Mode: string(types.PessimisticProofMode), + AggsenderPrivateKey: signertypes.SignerConfig{Method: signertypes.MethodNone}, + MaxCertSize: 100, + AggkitProverClient: aggkitgrpc.DefaultConfig(), + RequireCommitteeMembershipCheck: false, + }, + mockFn: func(mockCommittee *mocks.MultisigQuerier) { + signers := []*types.SignerInfo{ + types.NewSignerInfo("http://signer2", common.HexToAddress("0x2222222222222222222222222222222222222222")), + types.NewSignerInfo("http://signer3", common.HexToAddress("0x3333333333333333333333333333333333333333")), + types.NewSignerInfo("http://signer4", common.HexToAddress("0x4444444444444444444444444444444444")), + } + + committee, err := types.NewMultisigCommittee(signers, 2) + require.NoError(t, err) + mockCommittee.EXPECT().GetMultisigCommittee(mock.Anything, mock.Anything).Return(committee, nil).Maybe() + }, + }, + { + name: "not member of committee", + cfg: config.Config{ + Mode: string(types.PessimisticProofMode), + AggsenderPrivateKey: signertypes.SignerConfig{Method: signertypes.MethodNone}, + MaxCertSize: 100, + AggkitProverClient: aggkitgrpc.DefaultConfig(), + RequireCommitteeMembershipCheck: true, + }, + mockFn: func(mockCommittee *mocks.MultisigQuerier) { + signers := []*types.SignerInfo{ + types.NewSignerInfo("http://signer2", common.HexToAddress("0x2222222222222222222222222222222222222222")), + types.NewSignerInfo("http://signer3", common.HexToAddress("0x3333333333333333333333333333333333333333")), + types.NewSignerInfo("http://signer4", common.HexToAddress("0x4444444444444444444444444444444444444444")), + } + + committee, err := types.NewMultisigCommittee(signers, 2) + require.NoError(t, err) + mockCommittee.EXPECT().GetMultisigCommittee(mock.Anything, mock.Anything).Return(committee, nil).Maybe() + }, + expectedError: "signer address 0x0000000000000000000000000000000000000000 is not part of the multisig committee", }, { name: "error creating signer in PessimisticProofMode", @@ -84,6 +163,7 @@ func TestNewFlow(t *testing.T) { mockL1InfoTreeSyncer := mocks.NewL1InfoTreeSyncer(t) mockL2BridgeSyncer := mocks.NewL2BridgeSyncer(t) mockRollupDataQuerier := mocks.NewRollupDataQuerier(t) + mockCommitteeQuerier := mocks.NewMultisigQuerier(t) mockL2BridgeSyncer.EXPECT().OriginNetwork().Return(1).Maybe() mockLogger := log.WithFields("test", "NewFlow") @@ -93,6 +173,11 @@ func TestNewFlow(t *testing.T) { mockL2Client.EXPECT().CallContract(mock.Anything, mock.Anything, mock.Anything).Return([]byte{1, 2, 3}, nil).Maybe() mockL2Client.EXPECT().CodeAt(mock.Anything, mock.Anything, mock.Anything).Return([]byte{1, 2, 3}, nil).Maybe() mockRollupDataQuerier.EXPECT().GetRollupChainID().Return(uint64(1234), nil).Maybe() + + if tc.mockFn != nil { + tc.mockFn(mockCommitteeQuerier) + } + flow, err := NewFlow( ctx, tc.cfg, @@ -103,6 +188,7 @@ func TestNewFlow(t *testing.T) { mockL1InfoTreeSyncer, mockL2BridgeSyncer, mockRollupDataQuerier, + mockCommitteeQuerier, ) if tc.expectedError != "" { diff --git a/aggsender/types/multisig_committee.go b/aggsender/types/multisig_committee.go index d6be4500c..329e2ff4e 100644 --- a/aggsender/types/multisig_committee.go +++ b/aggsender/types/multisig_committee.go @@ -100,3 +100,22 @@ func (m *MultisigCommittee) Signers() []SignerInfo { func (m *MultisigCommittee) Size() int { return len(m.signers) } + +// IsMember checks if the given address is part of the committee +func (m *MultisigCommittee) IsMember(address common.Address) bool { + _, exists := m.signersSet[address] + return exists +} + +// String returns a string representation of the committee +func (m *MultisigCommittee) String() string { + s := "[Committee: " + for i, signer := range m.signers { + s += signer.Address.Hex() + if i < len(m.signers)-1 { + s += ", " + } + } + s += fmt.Sprintf(" Threshold: %d]", m.threshold) + return s +} diff --git a/aggsender/types/multisig_committee_test.go b/aggsender/types/multisig_committee_test.go index 39eca9f63..19b8fd2b6 100644 --- a/aggsender/types/multisig_committee_test.go +++ b/aggsender/types/multisig_committee_test.go @@ -129,3 +129,64 @@ func TestMultisigCommittee_Signers(t *testing.T) { cpySigners[0].Address = common.HexToAddress("0x4") require.NotEqual(t, signers, cpySigners) } + +func TestMultisigCommittee_IsMember(t *testing.T) { + s1 := NewSignerInfo("http://localhost:7001", common.HexToAddress("0x1")) + s2 := NewSignerInfo("http://localhost:7002", common.HexToAddress("0x2")) + + mc, err := NewMultisigCommittee([]*SignerInfo{s1}, 1) + require.NoError(t, err) + + // existing member + require.True(t, mc.IsMember(s1.Address)) + + // non-member + require.False(t, mc.IsMember(s2.Address)) + + // add new signer and verify membership + require.NoError(t, mc.AddSigner(s2)) + require.True(t, mc.IsMember(s2.Address)) + + // zero address should not be a member + require.False(t, mc.IsMember(common.Address{})) +} + +func TestMultisigCommittee_String(t *testing.T) { + s1 := NewSignerInfo("http://localhost:7001", common.HexToAddress("0x1")) + s2 := NewSignerInfo("http://localhost:7002", common.HexToAddress("0x2")) + s3 := NewSignerInfo("http://localhost:7003", common.HexToAddress("0x3")) + + tests := []struct { + name string + members []*SignerInfo + threshold uint32 + expected string + }{ + { + name: "single signer", + members: []*SignerInfo{s1}, + threshold: 1, + expected: "[Committee: " + s1.Address.Hex() + " Threshold: 1]", + }, + { + name: "two signers", + members: []*SignerInfo{s1, s2}, + threshold: 2, + expected: "[Committee: " + s1.Address.Hex() + ", " + s2.Address.Hex() + " Threshold: 2]", + }, + { + name: "three signers, threshold less than size", + members: []*SignerInfo{s1, s2, s3}, + threshold: 2, + expected: "[Committee: " + s1.Address.Hex() + ", " + s2.Address.Hex() + ", " + s3.Address.Hex() + " Threshold: 2]", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mc, err := NewMultisigCommittee(tc.members, tc.threshold) + require.NoError(t, err) + require.Equal(t, tc.expected, mc.String()) + }) + } +} diff --git a/aggsender/validator/config.go b/aggsender/validator/config.go index 2dd084546..5a2235f5f 100644 --- a/aggsender/validator/config.go +++ b/aggsender/validator/config.go @@ -42,6 +42,8 @@ type Config struct { AgglayerClient agglayer.ClientConfig `mapstructure:"AgglayerClient"` // Mode is the mode of the AggSender Validator (regular pessimistic proof mode or the aggchain proof mode) Mode string `jsonschema:"enum=PessimisticProof, enum=AggchainProof" mapstructure:"Mode"` + // RequireCommitteeMembershipCheck indicates whether to check if the validator is part of the committee + RequireCommitteeMembershipCheck bool `mapstructure:"RequireCommitteeMembershipCheck"` } type PPConfig struct { diff --git a/cmd/run.go b/cmd/run.go index 79100b71c..eb18fed86 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -95,6 +95,8 @@ func start(cliCtx *cli.Context) error { cliCtx.Context, components, cfg.L2GERSync, reorgDetectorL2, l2Client, l1InfoTreeSync, ) + committeeQuerier := runAggsenderMultisigCommitteeIfNeeded(components, cfg.L1NetworkConfig.RollupAddr, l1Client) + var rpcServices []jRPC.Service for _, component := range components { switch component { @@ -114,13 +116,6 @@ func start(cliCtx *cli.Context) error { go b.Start(cliCtx.Context) case aggkitcommon.AGGSENDER: - // TODO: Check if the RollupAddr is the right SC address, or we should use some other. - committeeQuerier, err := query.NewECDSAMultisigCommitteeQuery(cfg.L1NetworkConfig.RollupAddr, l1Client) - if err != nil { - return fmt.Errorf("failed to create ECDSA multisig committee querier (SC address: %s): %w", - cfg.L1NetworkConfig.RollupAddr, err) - } - aggsender, err := createAggSender( cliCtx.Context, cfg.AggSender, @@ -159,6 +154,7 @@ func start(cliCtx *cli.Context) error { l2BridgeSync, l1Client, rollupDataQuerier, + committeeQuerier, ) if err != nil { log.Fatal(err) @@ -223,6 +219,7 @@ func createAggSenderValidator(ctx context.Context, l2Syncer *bridgesync.BridgeSync, l1Client aggkittypes.BaseEthereumClienter, rollupDataQuerier *etherman.RollupDataQuerier, + committeeQuerier aggsendertypes.MultisigQuerier, ) (*aggsender.AggsenderValidator, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid aggsender validator config: %w", err) @@ -244,9 +241,10 @@ func createAggSenderValidator(ctx context.Context, commonFlowComponents, err = flows.CreateCommonFlowComponents( ctx, logger, nil, // storage is not used in validator, - l1Client, l1InfoTreeSync, l2Syncer, rollupDataQuerier, 0, false, + l1Client, l1InfoTreeSync, l2Syncer, rollupDataQuerier, committeeQuerier, 0, false, cfg.MaxCertSize, cfg.LerQuerier.RollupCreationBlockL1, cfg.DelayBetweenRetries.Duration, cfg.Signer, false, // full claims are not needed in validator mode + cfg.RequireCommitteeMembershipCheck, ) if err != nil { return nil, fmt.Errorf("failed to create common flow components: %w", err) @@ -266,10 +264,12 @@ func createAggSenderValidator(ctx context.Context, commonFlowComponents, err = flows.CreateCommonFlowComponents( ctx, logger, nil, // storage is not used in validator, - l1Client, l1InfoTreeSync, l2Syncer, rollupDataQuerier, 0, cfg.FEPConfig.RequireNoBlockGap, + l1Client, l1InfoTreeSync, l2Syncer, rollupDataQuerier, committeeQuerier, + 0, cfg.FEPConfig.RequireNoBlockGap, cfg.MaxCertSize, cfg.LerQuerier.RollupCreationBlockL1, cfg.DelayBetweenRetries.Duration, cfg.Signer, - false, // full claims are not needed in validator mode + false, // full claims are not needed in validator mode, + cfg.RequireCommitteeMembershipCheck, ) if err != nil { return nil, fmt.Errorf("failed to create common flow components: %w", err) @@ -726,6 +726,23 @@ func runBridgeSyncL2IfNeeded( return bridgeSyncL2 } +func runAggsenderMultisigCommitteeIfNeeded( + components []string, + rollupAddr common.Address, + l1Client aggkittypes.BaseEthereumClienter, +) aggsendertypes.MultisigQuerier { + if !isNeeded([]string{aggkitcommon.AGGSENDER, aggkitcommon.AGGSENDERVALIDATOR}, components) { + return nil + } + + committeeQuerier, err := query.NewECDSAMultisigCommitteeQuery(rollupAddr, l1Client) + if err != nil { + log.Fatalf("failed to create ECDSA multisig committee querier (SC address: %s): %s", rollupAddr, err) + } + + return committeeQuerier +} + func createBridgeService( cfg aggkitcommon.RESTConfig, l2NetworkID uint32, diff --git a/config/default.go b/config/default.go index 31c74b082..1c67038cd 100644 --- a/config/default.go +++ b/config/default.go @@ -204,6 +204,7 @@ RollupCreationBlockL1 = {{rollupCreationBlockNumber}} MaxL2BlockNumber = 0 StopOnFinishedSendingAllCertificates = false RequireValidatorCall = false +RequireCommitteeMembershipCheck = false [AggSender.RetriesToBuildAndSendCertificate] RetryMode = "delays" Delays = [ "1m", "1m", "2m", "5m", "5m", "8m" ] @@ -272,6 +273,7 @@ MaxL2BlockNumber = "{{AggSender.MaxL2BlockNumber}}" DelayBetweenRetries = "{{AggSender.DelayBetweenRetries}}" # PessimisticProof or AggchainProof Mode = "PessimisticProof" +RequireCommitteeMembershipCheck = {{AggSender.RequireCommitteeMembershipCheck}} [Validator.ServerConfig] Host = "0.0.0.0" Port = 5578