diff --git a/common/testcontainers/testcontainers.go b/common/testcontainers/testcontainers.go index 3a042a9c6..eb53152cd 100644 --- a/common/testcontainers/testcontainers.go +++ b/common/testcontainers/testcontainers.go @@ -21,9 +21,10 @@ import ( // TestcontainerApps testcontainers struct type TestcontainerApps struct { - postgresContainer *postgres.PostgresContainer - l2GethContainer *testcontainers.DockerContainer - poSL1Container compose.ComposeStack + postgresContainer *postgres.PostgresContainer + l2GethContainer *testcontainers.DockerContainer + poSL1Container compose.ComposeStack + web3SignerContainer *testcontainers.DockerContainer // common time stamp in nanoseconds. Timestamp int @@ -112,6 +113,47 @@ func (t *TestcontainerApps) StartPoSL1Container() error { return nil } +func (t *TestcontainerApps) StartWeb3SignerContainer(chainId int) error { + if t.web3SignerContainer != nil && t.web3SignerContainer.IsRunning() { + return nil + } + var ( + err error + rootDir string + ) + if rootDir, err = findProjectRootDir(); err != nil { + return fmt.Errorf("failed to find project root directory: %v", err) + } + + // web3signerconf/keyconf.yaml may contain multiple keys configured and web3signer then choses one corresponding to from field of tx + web3SignerConfDir := filepath.Join(rootDir, "common", "testcontainers", "web3signerconf") + + req := testcontainers.ContainerRequest{ + Image: "consensys/web3signer:develop", + ExposedPorts: []string{"9000/tcp"}, + Cmd: []string{"--key-config-path", "/web3signerconf/", "eth1", "--chain-id", fmt.Sprintf("%d", chainId)}, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: web3SignerConfDir, + ContainerFilePath: "/", + FileMode: 0o777, + }, + }, + WaitingFor: wait.ForLog("ready to handle signing requests"), + } + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + container, err := testcontainers.GenericContainer(context.Background(), genericContainerReq) + if err != nil { + log.Printf("failed to start web3signer container: %s", err) + return err + } + t.web3SignerContainer, _ = container.(*testcontainers.DockerContainer) + return nil +} + // GetPoSL1EndPoint returns the endpoint of the running PoS L1 endpoint func (t *TestcontainerApps) GetPoSL1EndPoint() (string, error) { if t.poSL1Container == nil { @@ -153,6 +195,14 @@ func (t *TestcontainerApps) GetL2GethEndPoint() (string, error) { return endpoint, nil } +// GetL2GethEndPoint returns the endpoint of the running L2Geth container +func (t *TestcontainerApps) GetWeb3SignerEndpoint() (string, error) { + if t.web3SignerContainer == nil || !t.web3SignerContainer.IsRunning() { + return "", errors.New("web3signer is not running") + } + return t.web3SignerContainer.PortEndpoint(context.Background(), "9000/tcp", "http") +} + // GetGormDBClient returns a gorm.DB by connecting to the running postgres container func (t *TestcontainerApps) GetGormDBClient() (*gorm.DB, error) { endpoint, err := t.GetDBEndPoint() @@ -201,6 +251,11 @@ func (t *TestcontainerApps) Free() { t.poSL1Container = nil } } + if t.web3SignerContainer != nil && t.web3SignerContainer.IsRunning() { + if err := t.web3SignerContainer.Terminate(ctx); err != nil { + log.Printf("failed to stop web3signer container: %s", err) + } + } } // findProjectRootDir find project root directory diff --git a/common/testcontainers/testcontainers_test.go b/common/testcontainers/testcontainers_test.go index 3eceb6b44..52271fb1c 100644 --- a/common/testcontainers/testcontainers_test.go +++ b/common/testcontainers/testcontainers_test.go @@ -44,6 +44,11 @@ func TestNewTestcontainerApps(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, ethclient) + assert.NoError(t, testApps.StartWeb3SignerContainer(1)) + endpoint, err = testApps.GetWeb3SignerEndpoint() + assert.NoError(t, err) + assert.NotEmpty(t, endpoint) + // test free testcontainers testApps.Free() endpoint, err = testApps.GetDBEndPoint() @@ -57,4 +62,8 @@ func TestNewTestcontainerApps(t *testing.T) { endpoint, err = testApps.GetPoSL1EndPoint() assert.EqualError(t, err, "PoS L1 container is not running") assert.Empty(t, endpoint) + + endpoint, err = testApps.GetWeb3SignerEndpoint() + assert.EqualError(t, err, "web3signer is not running") + assert.Empty(t, endpoint) } diff --git a/common/testcontainers/web3signerconf/keyconf.yaml b/common/testcontainers/web3signerconf/keyconf.yaml new file mode 100644 index 000000000..317f8763f --- /dev/null +++ b/common/testcontainers/web3signerconf/keyconf.yaml @@ -0,0 +1,7 @@ +type: "file-raw" +keyType: "SECP256K1" +privateKey: "0x1313131313131313131313131313131313131313131313131313131313131313" +--- +type: "file-raw" +keyType: "SECP256K1" +privateKey: "0x1212121212121212121212121212121212121212121212121212121212121212" \ No newline at end of file diff --git a/common/version/version.go b/common/version/version.go index a3a2028ff..a06c232c1 100644 --- a/common/version/version.go +++ b/common/version/version.go @@ -5,7 +5,7 @@ import ( "runtime/debug" ) -var tag = "v4.4.63" +var tag = "v4.4.64" var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok { diff --git a/rollup/conf/config.json b/rollup/conf/config.json index 457e42fe5..50d6b5726 100644 --- a/rollup/conf/config.json +++ b/rollup/conf/config.json @@ -24,7 +24,12 @@ "l1_base_fee_default": 15000000000, "l1_blob_base_fee_default": 1 }, - "gas_oracle_sender_private_key": "1313131313131313131313131313131313131313131313131313131313131313" + "gas_oracle_sender_signer_config": { + "signer_type": "PrivateKey", + "private_key_signer_config": { + "private_key": "1313131313131313131313131313131313131313131313131313131313131313" + } + } } }, "l2_config": { @@ -60,9 +65,24 @@ "enable_test_env_bypass_features": true, "finalize_batch_without_proof_timeout_sec": 7200, "finalize_bundle_without_proof_timeout_sec": 7200, - "gas_oracle_sender_private_key": "1313131313131313131313131313131313131313131313131313131313131313", - "commit_sender_private_key": "1414141414141414141414141414141414141414141414141414141414141414", - "finalize_sender_private_key": "1515151515151515151515151515151515151515151515151515151515151515", + "gas_oracle_sender_signer_config": { + "signer_type": "PrivateKey", + "private_key_signer_config": { + "private_key": "1313131313131313131313131313131313131313131313131313131313131313" + } + }, + "commit_sender_signer_config": { + "signer_type": "PrivateKey", + "private_key_signer_config": { + "private_key": "1414141414141414141414141414141414141414141414141414141414141414" + } + }, + "finalize_sender_signer_config": { + "signer_type": "PrivateKey", + "private_key_signer_config": { + "private_key": "1515151515151515151515151515151515151515151515151515151515151515" + } + }, "l1_commit_gas_limit_multiplier": 1.2 }, "chunk_proposer_config": { diff --git a/rollup/internal/config/config_test.go b/rollup/internal/config/config_test.go index 585a4e5c9..3374205b2 100644 --- a/rollup/internal/config/config_test.go +++ b/rollup/internal/config/config_test.go @@ -61,24 +61,25 @@ func TestConfig(t *testing.T) { assert.NoError(t, err) os.Setenv("SCROLL_ROLLUP_DB_CONFIG_DSN", "postgres://test:test@postgresql:5432/scroll?sslmode=disable") - os.Setenv("SCROLL_ROLLUP_L1_CONFIG_RELAYER_CONFIG_GAS_ORACLE_SENDER_PRIVATE_KEY", "1616161616161616161616161616161616161616161616161616161616161616") - os.Setenv("SCROLL_ROLLUP_L2_CONFIG_RELAYER_CONFIG_GAS_ORACLE_SENDER_PRIVATE_KEY", "1717171717171717171717171717171717171717171717171717171717171717") - os.Setenv("SCROLL_ROLLUP_L2_CONFIG_RELAYER_CONFIG_COMMIT_SENDER_PRIVATE_KEY", "1818181818181818181818181818181818181818181818181818181818181818") - os.Setenv("SCROLL_ROLLUP_L2_CONFIG_RELAYER_CONFIG_FINALIZE_SENDER_PRIVATE_KEY", "1919191919191919191919191919191919191919191919191919191919191919") + os.Setenv("SCROLL_ROLLUP_L1_CONFIG_RELAYER_CONFIG_GAS_ORACLE_SENDER_SIGNER_CONFIG_PRIVATE_KEY_SIGNER_CONFIG_PRIVATE_KEY", "1616161616161616161616161616161616161616161616161616161616161616") + os.Setenv("SCROLL_ROLLUP_L2_CONFIG_RELAYER_CONFIG_GAS_ORACLE_SENDER_SIGNER_CONFIG_PRIVATE_KEY_SIGNER_CONFIG_PRIVATE_KEY", "1717171717171717171717171717171717171717171717171717171717171717") + os.Setenv("SCROLL_ROLLUP_L2_CONFIG_RELAYER_CONFIG_COMMIT_SENDER_SIGNER_CONFIG_PRIVATE_KEY_SIGNER_CONFIG_PRIVATE_KEY", "1818181818181818181818181818181818181818181818181818181818181818") + os.Setenv("SCROLL_ROLLUP_L2_CONFIG_RELAYER_CONFIG_FINALIZE_SENDER_SIGNER_CONFIG_PRIVATE_KEY_SIGNER_CONFIG_PRIVATE_KEY", "1919191919191919191919191919191919191919191919191919191919191919") cfg2, err := NewConfig("../../conf/config.json") assert.NoError(t, err) assert.NotEqual(t, cfg.DBConfig.DSN, cfg2.DBConfig.DSN) - assert.NotEqual(t, cfg.L1Config.RelayerConfig.GasOracleSenderPrivateKey, cfg2.L1Config.RelayerConfig.GasOracleSenderPrivateKey) - assert.NotEqual(t, cfg.L2Config.RelayerConfig.GasOracleSenderPrivateKey, cfg2.L2Config.RelayerConfig.GasOracleSenderPrivateKey) - assert.NotEqual(t, cfg.L2Config.RelayerConfig.CommitSenderPrivateKey, cfg2.L2Config.RelayerConfig.CommitSenderPrivateKey) - assert.NotEqual(t, cfg.L2Config.RelayerConfig.FinalizeSenderPrivateKey, cfg2.L2Config.RelayerConfig.FinalizeSenderPrivateKey) + assert.NotEqual(t, cfg.L1Config.RelayerConfig.GasOracleSenderSignerConfig, cfg2.L1Config.RelayerConfig.GasOracleSenderSignerConfig) + assert.NotEqual(t, cfg.L2Config.RelayerConfig.GasOracleSenderSignerConfig, cfg2.L2Config.RelayerConfig.GasOracleSenderSignerConfig) + assert.NotEqual(t, cfg.L2Config.RelayerConfig.CommitSenderSignerConfig, cfg2.L2Config.RelayerConfig.CommitSenderSignerConfig) + assert.NotEqual(t, cfg.L2Config.RelayerConfig.FinalizeSenderSignerConfig, cfg2.L2Config.RelayerConfig.FinalizeSenderSignerConfig) assert.Equal(t, cfg2.DBConfig.DSN, "postgres://test:test@postgresql:5432/scroll?sslmode=disable") - assert.Equal(t, "1616161616161616161616161616161616161616161616161616161616161616", cfg2.L1Config.RelayerConfig.GasOracleSenderPrivateKey) - assert.Equal(t, "1717171717171717171717171717171717171717171717171717171717171717", cfg2.L2Config.RelayerConfig.GasOracleSenderPrivateKey) - assert.Equal(t, "1818181818181818181818181818181818181818181818181818181818181818", cfg2.L2Config.RelayerConfig.CommitSenderPrivateKey) - assert.Equal(t, "1919191919191919191919191919191919191919191919191919191919191919", cfg2.L2Config.RelayerConfig.FinalizeSenderPrivateKey) + assert.Equal(t, "1414141414141414141414141414141414141414141414141414141414141414", cfg.L2Config.RelayerConfig.CommitSenderSignerConfig.PrivateKeySignerConfig.PrivateKey) + assert.Equal(t, "1616161616161616161616161616161616161616161616161616161616161616", cfg2.L1Config.RelayerConfig.GasOracleSenderSignerConfig.PrivateKeySignerConfig.PrivateKey) + assert.Equal(t, "1717171717171717171717171717171717171717171717171717171717171717", cfg2.L2Config.RelayerConfig.GasOracleSenderSignerConfig.PrivateKeySignerConfig.PrivateKey) + assert.Equal(t, "1818181818181818181818181818181818181818181818181818181818181818", cfg2.L2Config.RelayerConfig.CommitSenderSignerConfig.PrivateKeySignerConfig.PrivateKey) + assert.Equal(t, "1919191919191919191919191919191919191919191919191919191919191919", cfg2.L2Config.RelayerConfig.FinalizeSenderSignerConfig.PrivateKeySignerConfig.PrivateKey) }) } diff --git a/rollup/internal/config/relayer.go b/rollup/internal/config/relayer.go index 02f828ba8..f6b022c54 100644 --- a/rollup/internal/config/relayer.go +++ b/rollup/internal/config/relayer.go @@ -54,10 +54,11 @@ type RelayerConfig struct { ChainMonitor *ChainMonitor `json:"chain_monitor"` // L1CommitGasLimitMultiplier multiplier for fallback gas limit in commitBatch txs L1CommitGasLimitMultiplier float64 `json:"l1_commit_gas_limit_multiplier,omitempty"` - // The private key of the relayer - GasOracleSenderPrivateKey string `json:"gas_oracle_sender_private_key"` - CommitSenderPrivateKey string `json:"commit_sender_private_key"` - FinalizeSenderPrivateKey string `json:"finalize_sender_private_key"` + + // Configs of transaction signers (GasOracle, Commit, Finalize) + GasOracleSenderSignerConfig *SignerConfig `json:"gas_oracle_sender_signer_config"` + CommitSenderSignerConfig *SignerConfig `json:"commit_sender_signer_config"` + FinalizeSenderSignerConfig *SignerConfig `json:"finalize_sender_signer_config"` // Indicates if bypass features specific to testing environments are enabled. EnableTestEnvBypassFeatures bool `json:"enable_test_env_bypass_features"` @@ -84,3 +85,21 @@ type GasOracleConfig struct { L1BaseFeeDefault uint64 `json:"l1_base_fee_default"` L1BlobBaseFeeDefault uint64 `json:"l1_blob_base_fee_default"` } + +// SignerConfig - config of signer, contains type and config corresponding to type +type SignerConfig struct { + SignerType string `json:"signer_type"` // type of signer can be PrivateKey or RemoteSigner + PrivateKeySignerConfig *PrivateKeySignerConfig `json:"private_key_signer_config"` + RemoteSignerConfig *RemoteSignerConfig `json:"remote_signer_config"` +} + +// PrivateKeySignerConfig - config of private signer, contains private key +type PrivateKeySignerConfig struct { + PrivateKey string `json:"private_key"` // private key of signer in case of PrivateKey signerType +} + +// RemoteSignerConfig - config of private signer, contains address and remote URL +type RemoteSignerConfig struct { + RemoteSignerUrl string `json:"remote_signer_url"` // remote signer url (web3signer) in case of RemoteSigner signerType + SignerAddress string `json:"signer_address"` // address of signer +} diff --git a/rollup/internal/controller/relayer/l1_relayer.go b/rollup/internal/controller/relayer/l1_relayer.go index 8dde48df6..d7f857263 100644 --- a/rollup/internal/controller/relayer/l1_relayer.go +++ b/rollup/internal/controller/relayer/l1_relayer.go @@ -10,8 +10,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/scroll-tech/go-ethereum/accounts/abi" - "github.com/scroll-tech/go-ethereum/common" - "github.com/scroll-tech/go-ethereum/crypto" "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/params" "gorm.io/gorm" @@ -54,18 +52,13 @@ type Layer1Relayer struct { // NewLayer1Relayer will return a new instance of Layer1RelayerClient func NewLayer1Relayer(ctx context.Context, db *gorm.DB, cfg *config.RelayerConfig, chainCfg *params.ChainConfig, serviceType ServiceType, reg prometheus.Registerer) (*Layer1Relayer, error) { var gasOracleSender *sender.Sender + var err error switch serviceType { case ServiceTypeL1GasOracle: - pKey, err := crypto.ToECDSA(common.FromHex(cfg.GasOracleSenderPrivateKey)) + gasOracleSender, err = sender.NewSender(ctx, cfg.SenderConfig, cfg.GasOracleSenderSignerConfig, "l1_relayer", "gas_oracle_sender", types.SenderTypeL1GasOracle, db, reg) if err != nil { - return nil, fmt.Errorf("new gas oracle sender failed, err: %v", err) - } - - gasOracleSender, err = sender.NewSender(ctx, cfg.SenderConfig, pKey, "l1_relayer", "gas_oracle_sender", types.SenderTypeL1GasOracle, db, reg) - if err != nil { - addr := crypto.PubkeyToAddress(pKey.PublicKey) - return nil, fmt.Errorf("new gas oracle sender failed for address %s, err: %v", addr.Hex(), err) + return nil, fmt.Errorf("new gas oracle sender failed, err: %w", err) } // Ensure test features aren't enabled on the scroll mainnet. diff --git a/rollup/internal/controller/relayer/l2_relayer.go b/rollup/internal/controller/relayer/l2_relayer.go index d7bcfd07e..63e5cbdce 100644 --- a/rollup/internal/controller/relayer/l2_relayer.go +++ b/rollup/internal/controller/relayer/l2_relayer.go @@ -2,7 +2,6 @@ package relayer import ( "context" - "crypto/ecdsa" "errors" "fmt" "math/big" @@ -76,18 +75,33 @@ type Layer2Relayer struct { // NewLayer2Relayer will return a new instance of Layer2RelayerClient func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.DB, cfg *config.RelayerConfig, chainCfg *params.ChainConfig, initGenesis bool, serviceType ServiceType, reg prometheus.Registerer) (*Layer2Relayer, error) { - gasOracleSenderPrivateKey, commitSenderPrivateKey, finalizeSenderPrivateKey, err := parsePrivateKeys(cfg) + + var gasOracleSender, commitSender, finalizeSender *sender.Sender + var err error + + // check that all 3 signer addresses are different, because there will be a problem in managing nonce for different senders + gasOracleSenderAddr, err := addrFromSignerConfig(cfg.GasOracleSenderSignerConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse addr from gas oracle signer config, err: %v", err) + } + commitSenderAddr, err := addrFromSignerConfig(cfg.CommitSenderSignerConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse addr from commit sender config, err: %v", err) + } + finalizeSenderAddr, err := addrFromSignerConfig(cfg.FinalizeSenderSignerConfig) if err != nil { - return nil, fmt.Errorf("failed to parse private keys provided by config, err: %v", err) + return nil, fmt.Errorf("failed to parse addr from finalize sender config, err: %v", err) + } + if gasOracleSenderAddr == commitSenderAddr || gasOracleSenderAddr == finalizeSenderAddr || commitSenderAddr == finalizeSenderAddr { + return nil, fmt.Errorf("gas oracle, commit, and finalize sender addresses must be different. Got: Gas Oracle=%s, Commit=%s, Finalize=%s", + gasOracleSenderAddr.Hex(), commitSenderAddr.Hex(), finalizeSenderAddr.Hex()) } - var gasOracleSender, commitSender, finalizeSender *sender.Sender switch serviceType { case ServiceTypeL2GasOracle: - gasOracleSender, err = sender.NewSender(ctx, cfg.SenderConfig, gasOracleSenderPrivateKey, "l2_relayer", "gas_oracle_sender", types.SenderTypeL2GasOracle, db, reg) + gasOracleSender, err = sender.NewSender(ctx, cfg.SenderConfig, cfg.GasOracleSenderSignerConfig, "l2_relayer", "gas_oracle_sender", types.SenderTypeL2GasOracle, db, reg) if err != nil { - addr := crypto.PubkeyToAddress(gasOracleSenderPrivateKey.PublicKey) - return nil, fmt.Errorf("new gas oracle sender failed for address %s, err: %w", addr.Hex(), err) + return nil, fmt.Errorf("new gas oracle sender failed, err: %w", err) } // Ensure test features aren't enabled on the ethereum mainnet. @@ -96,16 +110,14 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm. } case ServiceTypeL2RollupRelayer: - commitSender, err = sender.NewSender(ctx, cfg.SenderConfig, commitSenderPrivateKey, "l2_relayer", "commit_sender", types.SenderTypeCommitBatch, db, reg) + commitSender, err = sender.NewSender(ctx, cfg.SenderConfig, cfg.CommitSenderSignerConfig, "l2_relayer", "commit_sender", types.SenderTypeCommitBatch, db, reg) if err != nil { - addr := crypto.PubkeyToAddress(commitSenderPrivateKey.PublicKey) - return nil, fmt.Errorf("new commit sender failed for address %s, err: %w", addr.Hex(), err) + return nil, fmt.Errorf("new commit sender failed, err: %w", err) } - finalizeSender, err = sender.NewSender(ctx, cfg.SenderConfig, finalizeSenderPrivateKey, "l2_relayer", "finalize_sender", types.SenderTypeFinalizeBatch, db, reg) + finalizeSender, err = sender.NewSender(ctx, cfg.SenderConfig, cfg.FinalizeSenderSignerConfig, "l2_relayer", "finalize_sender", types.SenderTypeFinalizeBatch, db, reg) if err != nil { - addr := crypto.PubkeyToAddress(finalizeSenderPrivateKey.PublicKey) - return nil, fmt.Errorf("new finalize sender failed for address %s, err: %w", addr.Hex(), err) + return nil, fmt.Errorf("new finalize sender failed, err: %w", err) } // Ensure test features aren't enabled on the ethereum mainnet. @@ -1287,35 +1299,20 @@ func (r *Layer2Relayer) StopSenders() { } } -func parsePrivateKeys(cfg *config.RelayerConfig) (*ecdsa.PrivateKey, *ecdsa.PrivateKey, *ecdsa.PrivateKey, error) { - parseKey := func(hexKey string) (*ecdsa.PrivateKey, error) { - return crypto.ToECDSA(common.FromHex(hexKey)) - } - - gasOracleKey, err := parseKey(cfg.GasOracleSenderPrivateKey) - if err != nil { - return nil, nil, nil, fmt.Errorf("parse gas oracle sender private key failed: %w", err) - } - - commitKey, err := parseKey(cfg.CommitSenderPrivateKey) - if err != nil { - return nil, nil, nil, fmt.Errorf("parse commit sender private key failed: %w", err) - } - - finalizeKey, err := parseKey(cfg.FinalizeSenderPrivateKey) - if err != nil { - return nil, nil, nil, fmt.Errorf("parse finalize sender private key failed: %w", err) - } - - // Check if all three private keys are different - addrGasOracle := crypto.PubkeyToAddress(gasOracleKey.PublicKey) - addrCommit := crypto.PubkeyToAddress(commitKey.PublicKey) - addrFinalize := crypto.PubkeyToAddress(finalizeKey.PublicKey) - - if addrGasOracle == addrCommit || addrGasOracle == addrFinalize || addrCommit == addrFinalize { - return nil, nil, nil, fmt.Errorf("gas oracle, commit, and finalize sender addresses must be different. Got: Gas Oracle=%s, Commit=%s, Finalize=%s", - addrGasOracle.Hex(), addrCommit.Hex(), addrFinalize.Hex()) +func addrFromSignerConfig(config *config.SignerConfig) (common.Address, error) { + switch config.SignerType { + case sender.PrivateKeySignerType: + privKey, err := crypto.ToECDSA(common.FromHex(config.PrivateKeySignerConfig.PrivateKey)) + if err != nil { + return common.Address{}, fmt.Errorf("parse sender private key failed: %w", err) + } + return crypto.PubkeyToAddress(privKey.PublicKey), nil + case sender.RemoteSignerType: + if config.RemoteSignerConfig.SignerAddress == "" { + return common.Address{}, fmt.Errorf("signer address is empty") + } + return common.HexToAddress(config.RemoteSignerConfig.SignerAddress), nil + default: + return common.Address{}, fmt.Errorf("failed to determine signer address, unknown signer type: %v", config.SignerType) } - - return gasOracleKey, commitKey, finalizeKey, nil } diff --git a/rollup/internal/controller/sender/estimategas.go b/rollup/internal/controller/sender/estimategas.go index b1e368237..e0498cecb 100644 --- a/rollup/internal/controller/sender/estimategas.go +++ b/rollup/internal/controller/sender/estimategas.go @@ -25,8 +25,8 @@ func (s *Sender) estimateLegacyGas(to *common.Address, data []byte, fallbackGasL gasLimit, _, err := s.estimateGasLimit(to, data, nil, gasPrice, nil, nil, nil) if err != nil { - log.Error("estimateLegacyGas estimateGasLimit failure", "gas price", gasPrice, "from", s.auth.From.String(), - "nonce", s.auth.Nonce.Uint64(), "to address", to.String(), "fallback gas limit", fallbackGasLimit, "error", err) + log.Error("estimateLegacyGas estimateGasLimit failure", "gas price", gasPrice, "from", s.transactionSigner.GetAddr().String(), + "nonce", s.transactionSigner.GetNonce(), "to address", to.String(), "fallback gas limit", fallbackGasLimit, "error", err) if fallbackGasLimit == 0 { return nil, err } @@ -56,7 +56,7 @@ func (s *Sender) estimateDynamicGas(to *common.Address, data []byte, baseFee uin gasLimit, accessList, err := s.estimateGasLimit(to, data, nil, nil, gasTipCap, gasFeeCap, nil) if err != nil { log.Error("estimateDynamicGas estimateGasLimit failure", - "from", s.auth.From.String(), "nonce", s.auth.Nonce.Uint64(), "to address", to.String(), + "from", s.transactionSigner.GetAddr().String(), "nonce", s.transactionSigner.GetNonce(), "to address", to.String(), "fallback gas limit", fallbackGasLimit, "error", err) if fallbackGasLimit == 0 { return nil, err @@ -93,7 +93,7 @@ func (s *Sender) estimateBlobGas(to *common.Address, data []byte, sidecar *gethT gasLimit, accessList, err := s.estimateGasLimit(to, data, sidecar, nil, gasTipCap, gasFeeCap, blobGasFeeCap) if err != nil { log.Error("estimateBlobGas estimateGasLimit failure", - "from", s.auth.From.String(), "nonce", s.auth.Nonce.Uint64(), "to address", to.String(), + "from", s.transactionSigner.GetAddr().String(), "nonce", s.transactionSigner.GetNonce(), "to address", to.String(), "fallback gas limit", fallbackGasLimit, "error", err) if fallbackGasLimit == 0 { return nil, err @@ -117,7 +117,7 @@ func (s *Sender) estimateBlobGas(to *common.Address, data []byte, sidecar *gethT func (s *Sender) estimateGasLimit(to *common.Address, data []byte, sidecar *gethTypes.BlobTxSidecar, gasPrice, gasTipCap, gasFeeCap, blobGasFeeCap *big.Int) (uint64, *types.AccessList, error) { msg := ethereum.CallMsg{ - From: s.auth.From, + From: s.transactionSigner.GetAddr(), To: to, GasPrice: gasPrice, GasTipCap: gasTipCap, @@ -136,7 +136,8 @@ func (s *Sender) estimateGasLimit(to *common.Address, data []byte, sidecar *geth return 0, nil, err } - if s.config.TxType == LegacyTxType { + if s.config.TxType == LegacyTxType || + s.transactionSigner.GetType() == RemoteSignerType { // web3signer doesn't support access list return gasLimitWithoutAccessList, nil, nil } diff --git a/rollup/internal/controller/sender/sender.go b/rollup/internal/controller/sender/sender.go index 73165ab96..2c206a8d6 100644 --- a/rollup/internal/controller/sender/sender.go +++ b/rollup/internal/controller/sender/sender.go @@ -3,7 +3,6 @@ package sender import ( "bytes" "context" - "crypto/ecdsa" "errors" "fmt" "math/big" @@ -12,7 +11,6 @@ import ( "github.com/holiman/uint256" "github.com/prometheus/client_golang/prometheus" - "github.com/scroll-tech/go-ethereum/accounts/abi/bind" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/consensus/misc" gethTypes "github.com/scroll-tech/go-ethereum/core/types" @@ -67,16 +65,15 @@ type FeeData struct { // Sender Transaction sender to send transaction to l1/l2 geth type Sender struct { - config *config.SenderConfig - gethClient *gethclient.Client - client *ethclient.Client // The client to retrieve on chain data or send transaction. - chainID *big.Int // The chain id of the endpoint - ctx context.Context - service string - name string - senderType types.SenderType - - auth *bind.TransactOpts + config *config.SenderConfig + gethClient *gethclient.Client + client *ethclient.Client // The client to retrieve on chain data or send transaction. + transactionSigner *TransactionSigner + chainID *big.Int // The chain id of the endpoint + ctx context.Context + service string + name string + senderType types.SenderType db *gorm.DB pendingTransactionOrm *orm.PendingTransaction @@ -88,7 +85,7 @@ type Sender struct { } // NewSender returns a new instance of transaction sender -func NewSender(ctx context.Context, config *config.SenderConfig, priv *ecdsa.PrivateKey, service, name string, senderType types.SenderType, db *gorm.DB, reg prometheus.Registerer) (*Sender, error) { +func NewSender(ctx context.Context, config *config.SenderConfig, signerConfig *config.SignerConfig, service, name string, senderType types.SenderType, db *gorm.DB, reg prometheus.Registerer) (*Sender, error) { if config.EscalateMultipleNum <= config.EscalateMultipleDen { return nil, fmt.Errorf("invalid params, EscalateMultipleNum; %v, EscalateMultipleDen: %v", config.EscalateMultipleNum, config.EscalateMultipleDen) } @@ -103,18 +100,17 @@ func NewSender(ctx context.Context, config *config.SenderConfig, priv *ecdsa.Pri if err != nil { return nil, fmt.Errorf("failed to get chain ID, err: %w", err) } - - auth, err := bind.NewKeyedTransactorWithChainID(priv, chainID) + transactionSigner, err := NewTransactionSigner(signerConfig, chainID) if err != nil { - return nil, fmt.Errorf("failed to create transactor with chain ID %v, err: %w", chainID, err) + return nil, fmt.Errorf("failed to create transaction signer, err: %w", err) } // Set pending nonce - nonce, err := client.PendingNonceAt(ctx, auth.From) + nonce, err := client.PendingNonceAt(ctx, transactionSigner.GetAddr()) if err != nil { - return nil, fmt.Errorf("failed to get pending nonce for address %s, err: %w", auth.From.Hex(), err) + return nil, fmt.Errorf("failed to get pending nonce for address %s, err: %w", transactionSigner.GetAddr(), err) } - auth.Nonce = big.NewInt(int64(nonce)) + transactionSigner.SetNonce(nonce) sender := &Sender{ ctx: ctx, @@ -122,7 +118,7 @@ func NewSender(ctx context.Context, config *config.SenderConfig, priv *ecdsa.Pri gethClient: gethclient.New(rpcClient), client: client, chainID: chainID, - auth: auth, + transactionSigner: transactionSigner, db: db, pendingTransactionOrm: orm.NewPendingTransaction(db), confirmCh: make(chan *Confirmation, 128), @@ -146,7 +142,7 @@ func (s *Sender) GetChainID() *big.Int { // Stop stop the sender module. func (s *Sender) Stop() { close(s.stopCh) - log.Info("sender stopped", "name", s.name, "service", s.service, "address", s.auth.From.String()) + log.Info("sender stopped", "name", s.name, "service", s.service, "address", s.transactionSigner.GetAddr().String()) } // ConfirmChan channel used to communicate with transaction sender @@ -217,18 +213,18 @@ func (s *Sender) SendTransaction(contextID string, target *common.Address, data if feeData, err = s.getFeeData(target, data, sidecar, baseFee, blobBaseFee, fallbackGasLimit); err != nil { s.metrics.sendTransactionFailureGetFee.WithLabelValues(s.service, s.name).Inc() - log.Error("failed to get fee data", "from", s.auth.From.String(), "nonce", s.auth.Nonce.Uint64(), "fallback gas limit", fallbackGasLimit, "err", err) + log.Error("failed to get fee data", "from", s.transactionSigner.GetAddr().String(), "nonce", s.transactionSigner.GetNonce(), "fallback gas limit", fallbackGasLimit, "err", err) return common.Hash{}, fmt.Errorf("failed to get fee data, err: %w", err) } if tx, err = s.createAndSendTx(feeData, target, data, sidecar, nil); err != nil { s.metrics.sendTransactionFailureSendTx.WithLabelValues(s.service, s.name).Inc() - log.Error("failed to create and send tx (non-resubmit case)", "from", s.auth.From.String(), "nonce", s.auth.Nonce.Uint64(), "err", err) + log.Error("failed to create and send tx (non-resubmit case)", "from", s.transactionSigner.GetAddr().String(), "nonce", s.transactionSigner.GetNonce(), "err", err) return common.Hash{}, fmt.Errorf("failed to create and send transaction, err: %w", err) } if err = s.pendingTransactionOrm.InsertPendingTransaction(s.ctx, contextID, s.getSenderMeta(), tx, blockNumber); err != nil { - log.Error("failed to insert transaction", "from", s.auth.From.String(), "nonce", s.auth.Nonce.Uint64(), "err", err) + log.Error("failed to insert transaction", "from", s.transactionSigner.GetAddr().String(), "nonce", s.transactionSigner.GetNonce(), "err", err) return common.Hash{}, fmt.Errorf("failed to insert transaction, err: %w", err) } return tx.Hash(), nil @@ -236,7 +232,7 @@ func (s *Sender) SendTransaction(contextID string, target *common.Address, data func (s *Sender) createAndSendTx(feeData *FeeData, target *common.Address, data []byte, sidecar *gethTypes.BlobTxSidecar, overrideNonce *uint64) (*gethTypes.Transaction, error) { var ( - nonce = s.auth.Nonce.Uint64() + nonce = s.transactionSigner.GetNonce() txData gethTypes.TxData ) @@ -268,7 +264,7 @@ func (s *Sender) createAndSendTx(feeData *FeeData, target *common.Address, data } } else { if target == nil { - log.Error("blob transaction to address cannot be nil", "address", s.auth.From.String(), "chainID", s.chainID.Uint64(), "nonce", s.auth.Nonce.Uint64()) + log.Error("blob transaction to address cannot be nil", "address", s.transactionSigner.GetAddr().String(), "chainID", s.chainID.Uint64(), "nonce", s.transactionSigner.GetNonce()) return nil, errors.New("blob transaction to address cannot be nil") } @@ -289,14 +285,15 @@ func (s *Sender) createAndSendTx(feeData *FeeData, target *common.Address, data } // sign and send - signedTx, err := s.auth.Signer(s.auth.From, gethTypes.NewTx(txData)) + tx := gethTypes.NewTx(txData) + signedTx, err := s.transactionSigner.SignTransaction(s.ctx, tx) if err != nil { - log.Error("failed to sign tx", "address", s.auth.From.String(), "err", err) + log.Error("failed to sign tx", "address", s.transactionSigner.GetAddr().String(), "err", err) return nil, err } if err = s.client.SendTransaction(s.ctx, signedTx); err != nil { - log.Error("failed to send tx", "tx hash", signedTx.Hash().String(), "from", s.auth.From.String(), "nonce", signedTx.Nonce(), "err", err) + log.Error("failed to send tx", "tx hash", signedTx.Hash().String(), "from", s.transactionSigner.GetAddr().String(), "nonce", signedTx.Nonce(), "err", err) // Check if contain nonce, and reset nonce // only reset nonce when it is not from resubmit if strings.Contains(err.Error(), "nonce too low") && overrideNonce == nil { @@ -325,19 +322,19 @@ func (s *Sender) createAndSendTx(feeData *FeeData, target *common.Address, data // update nonce when it is not from resubmit if overrideNonce == nil { - s.auth.Nonce = big.NewInt(int64(nonce + 1)) + s.transactionSigner.SetNonce(nonce + 1) } return signedTx, nil } // resetNonce reset nonce if send signed tx failed. func (s *Sender) resetNonce(ctx context.Context) { - nonce, err := s.client.PendingNonceAt(ctx, s.auth.From) + nonce, err := s.client.PendingNonceAt(ctx, s.transactionSigner.GetAddr()) if err != nil { - log.Warn("failed to reset nonce", "address", s.auth.From.String(), "err", err) + log.Warn("failed to reset nonce", "address", s.transactionSigner.GetAddr().String(), "err", err) return } - s.auth.Nonce = big.NewInt(int64(nonce)) + s.transactionSigner.SetNonce(nonce) } func (s *Sender) resubmitTransaction(tx *gethTypes.Transaction, baseFee, blobBaseFee uint64) (*gethTypes.Transaction, error) { @@ -349,7 +346,7 @@ func (s *Sender) resubmitTransaction(tx *gethTypes.Transaction, baseFee, blobBas txInfo := map[string]interface{}{ "tx_hash": tx.Hash().String(), "tx_type": s.config.TxType, - "from": s.auth.From.String(), + "from": s.transactionSigner.GetAddr().String(), "nonce": tx.Nonce(), } @@ -473,7 +470,7 @@ func (s *Sender) resubmitTransaction(tx *gethTypes.Transaction, baseFee, blobBas s.metrics.resubmitTransactionTotal.WithLabelValues(s.service, s.name).Inc() tx, err := s.createAndSendTx(&feeData, tx.To(), tx.Data(), tx.BlobTxSidecar(), &nonce) if err != nil { - log.Error("failed to create and send tx (resubmit case)", "from", s.auth.From.String(), "nonce", nonce, "err", err) + log.Error("failed to create and send tx (resubmit case)", "from", s.transactionSigner.GetAddr().String(), "nonce", nonce, "err", err) return nil, err } return tx, nil @@ -515,7 +512,7 @@ func (s *Sender) checkPendingTransaction() { err := s.db.Transaction(func(dbTX *gorm.DB) error { // Update the status of the transaction to TxStatusConfirmed. if err := s.pendingTransactionOrm.UpdatePendingTransactionStatusByTxHash(s.ctx, tx.Hash(), types.TxStatusConfirmed, dbTX); err != nil { - log.Error("failed to update transaction status by tx hash", "hash", tx.Hash().String(), "sender meta", s.getSenderMeta(), "from", s.auth.From.String(), "nonce", tx.Nonce(), "err", err) + log.Error("failed to update transaction status by tx hash", "hash", tx.Hash().String(), "sender meta", s.getSenderMeta(), "from", s.transactionSigner.GetAddr().String(), "nonce", tx.Nonce(), "err", err) return err } // Update other transactions with the same nonce and sender address as failed. @@ -572,7 +569,7 @@ func (s *Sender) checkPendingTransaction() { "service", s.service, "name", s.name, "hash", tx.Hash().String(), - "from", s.auth.From.String(), + "from", s.transactionSigner.GetAddr().String(), "nonce", tx.Nonce(), "submitBlockNumber", txnToCheck.SubmitBlockNumber, "currentBlockNumber", blockNumber, @@ -580,7 +577,7 @@ func (s *Sender) checkPendingTransaction() { if newTx, err := s.resubmitTransaction(tx, baseFee, blobBaseFee); err != nil { s.metrics.resubmitTransactionFailedTotal.WithLabelValues(s.service, s.name).Inc() - log.Error("failed to resubmit transaction", "context ID", txnToCheck.ContextID, "sender meta", s.getSenderMeta(), "from", s.auth.From.String(), "nonce", tx.Nonce(), "err", err) + log.Error("failed to resubmit transaction", "context ID", txnToCheck.ContextID, "sender meta", s.getSenderMeta(), "from", s.transactionSigner.GetAddr().String(), "nonce", tx.Nonce(), "err", err) } else { err := s.db.Transaction(func(dbTX *gorm.DB) error { // Update the status of the original transaction as replaced, while still checking its confirmation status. @@ -623,7 +620,7 @@ func (s *Sender) getSenderMeta() *orm.SenderMeta { return &orm.SenderMeta{ Name: s.name, Service: s.service, - Address: s.auth.From, + Address: s.transactionSigner.GetAddr(), Type: s.senderType, } } diff --git a/rollup/internal/controller/sender/sender_test.go b/rollup/internal/controller/sender/sender_test.go index 2d3573c32..6f2af46c2 100644 --- a/rollup/internal/controller/sender/sender_test.go +++ b/rollup/internal/controller/sender/sender_test.go @@ -38,7 +38,9 @@ import ( ) var ( + privateKeyString string privateKey *ecdsa.PrivateKey + signerConfig *config.SignerConfig cfg *config.Config testApps *testcontainers.TestcontainerApps txTypes = []string{"LegacyTx", "DynamicFeeTx", "DynamicFeeTx"} @@ -53,6 +55,9 @@ func TestMain(m *testing.M) { if testApps != nil { testApps.Free() } + if testAppsSignerTest != nil { + testAppsSignerTest.Free() + } }() m.Run() } @@ -65,7 +70,14 @@ func setupEnv(t *testing.T) { var err error cfg, err = config.NewConfig("../../../conf/config.json") assert.NoError(t, err) - priv, err := crypto.HexToECDSA("1212121212121212121212121212121212121212121212121212121212121212") + privateKeyString = "1212121212121212121212121212121212121212121212121212121212121212" + signerConfig = &config.SignerConfig{ + SignerType: "PrivateKey", + PrivateKeySignerConfig: &config.PrivateKeySignerConfig{ + PrivateKey: privateKeyString, + }, + } + priv, err := crypto.HexToECDSA(privateKeyString) assert.NoError(t, err) privateKey = priv @@ -148,7 +160,7 @@ func testNewSender(t *testing.T) { // exit by Stop() cfgCopy1 := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy1.TxType = txType - newSender1, err := NewSender(context.Background(), &cfgCopy1, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + newSender1, err := NewSender(context.Background(), &cfgCopy1, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) newSender1.Stop() @@ -156,7 +168,7 @@ func testNewSender(t *testing.T) { cfgCopy2 := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy2.TxType = txType subCtx, cancel := context.WithCancel(context.Background()) - _, err = NewSender(subCtx, &cfgCopy2, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + _, err = NewSender(subCtx, &cfgCopy2, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) cancel() } @@ -170,7 +182,7 @@ func testSendAndRetrieveTransaction(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) hash, err := s.SendTransaction("0", &common.Address{}, nil, txBlob[i], 0) @@ -206,7 +218,7 @@ func testFallbackGasLimit(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.Confirmations = rpc.LatestBlockNumber - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) client, err := ethclient.Dial(cfgCopy.Endpoint) @@ -262,7 +274,7 @@ func testResubmitZeroGasPriceTransaction(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(0), @@ -302,7 +314,7 @@ func testAccessListTransactionGasLimit(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) l2GasOracleABI, err := bridgeAbi.L2GasPriceOracleMetaData.GetAbi() @@ -343,7 +355,7 @@ func testResubmitNonZeroGasPriceTransaction(t *testing.T) { cfgCopy.EscalateMultipleNum = 110 cfgCopy.EscalateMultipleDen = 100 cfgCopy.TxType = txType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(1000000000), @@ -392,7 +404,7 @@ func testResubmitUnderpricedTransaction(t *testing.T) { cfgCopy.EscalateMultipleNum = 109 cfgCopy.EscalateMultipleDen = 100 cfgCopy.TxType = txType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(1000000000), @@ -429,7 +441,7 @@ func testResubmitDynamicFeeTransactionWithRisingBaseFee(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) patchGuard := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { @@ -438,7 +450,7 @@ func testResubmitDynamicFeeTransactionWithRisingBaseFee(t *testing.T) { defer patchGuard.Reset() tx := gethTypes.NewTx(&gethTypes.DynamicFeeTx{ - Nonce: s.auth.Nonce.Uint64(), + Nonce: s.transactionSigner.GetNonce(), To: &common.Address{}, Data: nil, Gas: 21000, @@ -471,7 +483,7 @@ func testResubmitBlobTransactionWithRisingBaseFeeAndBlobBaseFee(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = DynamicFeeTxType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) patchGuard := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { @@ -483,7 +495,7 @@ func testResubmitBlobTransactionWithRisingBaseFeeAndBlobBaseFee(t *testing.T) { assert.NoError(t, err) tx := gethTypes.NewTx(&gethTypes.BlobTx{ ChainID: uint256.MustFromBig(s.chainID), - Nonce: s.auth.Nonce.Uint64(), + Nonce: s.transactionSigner.GetNonce(), GasTipCap: uint256.MustFromBig(big.NewInt(0)), GasFeeCap: uint256.MustFromBig(big.NewInt(0)), Gas: 21000, @@ -539,7 +551,7 @@ func testResubmitNonceGappedTransaction(t *testing.T) { // stop background check pending transaction cfgCopy.CheckPendingTime = math.MaxUint32 - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeUnknown, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { @@ -588,7 +600,7 @@ func testCheckPendingTransactionTxConfirmed(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeCommitBatch, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeCommitBatch, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { @@ -630,7 +642,7 @@ func testCheckPendingTransactionResubmitTxConfirmed(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.EscalateBlocks = 0 - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeFinalizeBatch, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeFinalizeBatch, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { @@ -690,7 +702,7 @@ func testCheckPendingTransactionReplacedTxConfirmed(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.EscalateBlocks = 0 - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeL1GasOracle, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeL1GasOracle, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { @@ -760,7 +772,7 @@ func testCheckPendingTransactionTxMultipleTimesWithOnlyOneTxPending(t *testing.T cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.EscalateBlocks = 0 - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeCommitBatch, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeCommitBatch, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { @@ -837,7 +849,7 @@ func testBlobTransactionWithBlobhashOpContractCall(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = DynamicFeeTxType - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeL1GasOracle, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeL1GasOracle, db, nil) assert.NoError(t, err) defer s.Stop() @@ -889,7 +901,7 @@ func testSendBlobCarryingTxOverLimit(t *testing.T) { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) - s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", types.SenderTypeCommitBatch, db, nil) + s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeCommitBatch, db, nil) assert.NoError(t, err) for i := 0; i < int(cfgCopy.MaxPendingBlobTxs); i++ { diff --git a/rollup/internal/controller/sender/transaction_signer.go b/rollup/internal/controller/sender/transaction_signer.go new file mode 100644 index 000000000..5bc4c5df1 --- /dev/null +++ b/rollup/internal/controller/sender/transaction_signer.go @@ -0,0 +1,156 @@ +package sender + +import ( + "context" + "fmt" + "math/big" + + "github.com/scroll-tech/go-ethereum/accounts/abi/bind" + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/common/hexutil" + gethTypes "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/crypto" + "github.com/scroll-tech/go-ethereum/log" + "github.com/scroll-tech/go-ethereum/rpc" + + "scroll-tech/rollup/internal/config" +) + +const ( + // PrivateKeySignerType + PrivateKeySignerType = "PrivateKey" + + // RemoteSignerType + RemoteSignerType = "RemoteSigner" +) + +// TransactionSigner signs given transactions +type TransactionSigner struct { + config *config.SignerConfig + auth *bind.TransactOpts + rpcClient *rpc.Client + nonce uint64 + addr common.Address +} + +func NewTransactionSigner(config *config.SignerConfig, chainID *big.Int) (*TransactionSigner, error) { + switch config.SignerType { + case PrivateKeySignerType: + privKey, err := crypto.ToECDSA(common.FromHex(config.PrivateKeySignerConfig.PrivateKey)) + if err != nil { + return nil, fmt.Errorf("parse sender private key failed: %w", err) + } + auth, err := bind.NewKeyedTransactorWithChainID(privKey, chainID) + if err != nil { + return nil, fmt.Errorf("failed to create transactor with chain ID %v, err: %w", chainID, err) + } + return &TransactionSigner{ + config: config, + auth: auth, + addr: crypto.PubkeyToAddress(privKey.PublicKey), + }, nil + case RemoteSignerType: + if config.RemoteSignerConfig.SignerAddress == "" { + return nil, fmt.Errorf("failed to create RemoteSigner, signer address is empty") + } + rpcClient, err := rpc.Dial(config.RemoteSignerConfig.RemoteSignerUrl) + if err != nil { + return nil, fmt.Errorf("failed to dial rpc client, err: %w", err) + } + return &TransactionSigner{ + config: config, + rpcClient: rpcClient, + addr: common.HexToAddress(config.RemoteSignerConfig.SignerAddress), + }, nil + default: + return nil, fmt.Errorf("failed to create new transaction signer, unknown type: %v", config.SignerType) + } +} + +func (ts *TransactionSigner) SignTransaction(ctx context.Context, tx *gethTypes.Transaction) (*gethTypes.Transaction, error) { + switch ts.config.SignerType { + case PrivateKeySignerType: + signedTx, err := ts.auth.Signer(ts.addr, tx) + if err != nil { + log.Info("failed to sign tx", "address", ts.addr.String(), "err", err) + return nil, err + } + return signedTx, nil + case RemoteSignerType: + rpcTx, err := txDataToRpcTx(&ts.addr, tx) + if err != nil { + return nil, fmt.Errorf("failed to convert txData to rpc transaction, err: %w", err) + } + var result hexutil.Bytes + err = ts.rpcClient.CallContext(ctx, &result, "eth_signTransaction", rpcTx) + if err != nil { + log.Info("failed to call remote rpc", "err", err) + return nil, err + } + signedTx := new(gethTypes.Transaction) + if err := signedTx.UnmarshalBinary(result); err != nil { + return nil, err + } + return signedTx, nil + default: + // this shouldn't happen, because SignerType is checked during creation + return nil, fmt.Errorf("shouldn't happen, unknown signer type") + } +} + +func (ts *TransactionSigner) SetNonce(nonce uint64) { + ts.nonce = nonce +} + +func (ts *TransactionSigner) GetNonce() uint64 { + return ts.nonce +} + +func (ts *TransactionSigner) GetAddr() common.Address { + return ts.addr +} + +func (ts *TransactionSigner) GetType() string { + return ts.config.SignerType +} + +// RpcTransaction transaction that will be send through rpc to web3Signer +type RpcTransaction struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas uint64 `json:"gas"` + GasPrice *big.Int `json:"gasPrice,omitempty"` + MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas,omitempty"` + MaxFeePerGas *big.Int `json:"maxFeePerGas,omitempty"` + Nonce uint64 `json:"nonce"` + Value *big.Int `json:"value"` + Data string `json:"data"` +} + +func txDataToRpcTx(from *common.Address, tx *gethTypes.Transaction) (*RpcTransaction, error) { + switch tx.Type() { + case gethTypes.LegacyTxType: + return &RpcTransaction{ + From: from, + To: tx.To(), + Gas: tx.Gas(), + GasPrice: tx.GasPrice(), + Nonce: tx.Nonce(), + Value: tx.Value(), + Data: common.Bytes2Hex(tx.Data()), + }, nil + case gethTypes.DynamicFeeTxType: + return &RpcTransaction{ + From: from, + To: tx.To(), + Gas: tx.Gas(), + MaxPriorityFeePerGas: tx.GasTipCap(), + MaxFeePerGas: tx.GasFeeCap(), + Nonce: tx.Nonce(), + Value: tx.Value(), + Data: common.Bytes2Hex(tx.Data()), + }, nil + default: // other tx types (BlobTx) currently not supported by web3signer + return nil, fmt.Errorf("failed to convert tx to RpcTransaction, unsupported tx type, %d", tx.Type()) + } +} diff --git a/rollup/internal/controller/sender/transaction_signer_test.go b/rollup/internal/controller/sender/transaction_signer_test.go new file mode 100644 index 000000000..5937cb8c8 --- /dev/null +++ b/rollup/internal/controller/sender/transaction_signer_test.go @@ -0,0 +1,122 @@ +package sender + +import ( + "context" + "math/big" + "os" + "testing" + + "github.com/holiman/uint256" + "github.com/scroll-tech/go-ethereum/common" + gethTypes "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/log" + "github.com/stretchr/testify/assert" + + "scroll-tech/common/testcontainers" + + "scroll-tech/rollup/internal/config" +) + +var ( + testAppsSignerTest *testcontainers.TestcontainerApps + chainId int +) + +func setupEnvSignerTest(t *testing.T) { + glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat())) + glogger.Verbosity(log.LvlInfo) + log.Root().SetHandler(glogger) + + chainId = 1 + testAppsSignerTest = testcontainers.NewTestcontainerApps() + assert.NoError(t, testAppsSignerTest.StartWeb3SignerContainer(chainId)) +} + +func TestTransactionSigner(t *testing.T) { + setupEnvSignerTest(t) + t.Run("test both signer types", testBothSignerTypes) +} + +func testBothSignerTypes(t *testing.T) { + endpoint, err := testAppsSignerTest.GetWeb3SignerEndpoint() + assert.NoError(t, err) + + // create remote signer + remoteSignerConf := &config.SignerConfig{ + SignerType: RemoteSignerType, + RemoteSignerConfig: &config.RemoteSignerConfig{ + SignerAddress: "0x1C5A77d9FA7eF466951B2F01F724BCa3A5820b63", + RemoteSignerUrl: endpoint, + }, + } + remoteSigner, err := NewTransactionSigner(remoteSignerConf, big.NewInt(int64(chainId))) + assert.NoError(t, err) + remoteSigner.SetNonce(2) + + // create private key signer + privateKeySignerConf := &config.SignerConfig{ + SignerType: PrivateKeySignerType, + PrivateKeySignerConfig: &config.PrivateKeySignerConfig{ + PrivateKey: "1212121212121212121212121212121212121212121212121212121212121212", + }, + } + privateKeySigner, err := NewTransactionSigner(privateKeySignerConf, big.NewInt(int64(chainId))) + assert.NoError(t, err) + privateKeySigner.SetNonce(2) + + assert.Equal(t, remoteSigner.GetAddr(), privateKeySigner.GetAddr()) + + to := common.BytesToAddress([]byte{0, 1, 2, 3}) + data := []byte("data") + + // check LegacyTx and DynamicFeeTx - transactions supported by web3signer + txDatas := []gethTypes.TxData{ + &gethTypes.LegacyTx{ + Nonce: remoteSigner.GetNonce(), + GasPrice: big.NewInt(1000), + Gas: 10000, + To: &to, + Data: data, + }, + &gethTypes.DynamicFeeTx{ + Nonce: remoteSigner.GetNonce(), + Gas: 10000, + To: &to, + Data: data, + ChainID: big.NewInt(int64(chainId)), + GasTipCap: big.NewInt(2000), + GasFeeCap: big.NewInt(3000), + }, + } + var signedTx1 *gethTypes.Transaction + var signedTx2 *gethTypes.Transaction + for _, txData := range txDatas { + tx := gethTypes.NewTx(txData) + + signedTx1, err = remoteSigner.SignTransaction(context.Background(), tx) + assert.NoError(t, err) + + signedTx2, err = privateKeySigner.SignTransaction(context.Background(), tx) + assert.NoError(t, err) + + assert.Equal(t, signedTx1.Hash(), signedTx2.Hash()) + } + + // BlobTx is not supported + txData := &gethTypes.BlobTx{ + Nonce: remoteSigner.GetNonce(), + Gas: 10000, + To: to, + Data: data, + ChainID: uint256.NewInt(1), + GasTipCap: uint256.NewInt(2000), + GasFeeCap: uint256.NewInt(3000), + BlobFeeCap: uint256.NewInt(1), + BlobHashes: []common.Hash{}, + Sidecar: nil, + } + tx := gethTypes.NewTx(txData) + + _, err = remoteSigner.SignTransaction(context.Background(), tx) + assert.Error(t, err) +} diff --git a/rollup/tests/bridge_test.go b/rollup/tests/bridge_test.go index e24edda94..3ab88f843 100644 --- a/rollup/tests/bridge_test.go +++ b/rollup/tests/bridge_test.go @@ -105,12 +105,12 @@ func setupEnv(t *testing.T) { l2Cfg.Confirmations = 0 l2Cfg.RelayerConfig.SenderConfig.Confirmations = 0 - pKey, err := crypto.ToECDSA(common.FromHex(l2Cfg.RelayerConfig.CommitSenderPrivateKey)) + pKey, err := crypto.ToECDSA(common.FromHex(l2Cfg.RelayerConfig.CommitSenderSignerConfig.PrivateKeySignerConfig.PrivateKey)) assert.NoError(t, err) l1Auth, err = bind.NewKeyedTransactorWithChainID(pKey, l1GethChainID) assert.NoError(t, err) - pKey, err = crypto.ToECDSA(common.FromHex(l2Cfg.RelayerConfig.GasOracleSenderPrivateKey)) + pKey, err = crypto.ToECDSA(common.FromHex(l2Cfg.RelayerConfig.GasOracleSenderSignerConfig.PrivateKeySignerConfig.PrivateKey)) assert.NoError(t, err) l2Auth, err = bind.NewKeyedTransactorWithChainID(pKey, l2GethChainID) assert.NoError(t, err) diff --git a/tests/integration-test/contracts_test.go b/tests/integration-test/contracts_test.go index f1c1412c9..4483f394b 100644 --- a/tests/integration-test/contracts_test.go +++ b/tests/integration-test/contracts_test.go @@ -68,7 +68,7 @@ func testGreeter(t *testing.T) { chainID, err := l2Cli.ChainID(context.Background()) assert.NoError(t, err) - pKey, err := crypto.ToECDSA(common.FromHex(rollupApp.Config.L2Config.RelayerConfig.CommitSenderPrivateKey)) + pKey, err := crypto.ToECDSA(common.FromHex(rollupApp.Config.L2Config.RelayerConfig.CommitSenderSignerConfig.PrivateKeySignerConfig.PrivateKey)) assert.NoError(t, err) auth, err := bind.NewKeyedTransactorWithChainID(pKey, chainID) assert.NoError(t, err)