diff --git a/arbnode/node.go b/arbnode/node.go index b0ee6ea6dd..7d3e37bd9e 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -510,6 +510,7 @@ type SequencerConfig struct { MaxBlockSpeed time.Duration `koanf:"max-block-speed"` MaxRevertGasReject uint64 `koanf:"max-revert-gas-reject"` MaxAcceptableTimestampDelta time.Duration `koanf:"max-acceptable-timestamp-delta"` + SenderWhitelist []string `koanf:"sender-whitelist"` Dangerous DangerousSequencerConfig `koanf:"dangerous"` } @@ -526,6 +527,7 @@ var TestSequencerConfig = SequencerConfig{ MaxBlockSpeed: time.Millisecond * 10, MaxRevertGasReject: params.TxGas + 10000, MaxAcceptableTimestampDelta: time.Hour, + SenderWhitelist: nil, Dangerous: TestDangerousSequencerConfig, } @@ -534,6 +536,7 @@ func SequencerConfigAddOptions(prefix string, f *flag.FlagSet) { f.Duration(prefix+".max-block-speed", DefaultSequencerConfig.MaxBlockSpeed, "minimum delay between blocks (sets a maximum speed of block production)") f.Uint64(prefix+".max-revert-gas-reject", DefaultSequencerConfig.MaxRevertGasReject, "maximum gas executed in a revert for the sequencer to reject the transaction instead of posting it (anti-DOS)") f.Duration(prefix+".max-acceptable-timestamp-delta", DefaultSequencerConfig.MaxAcceptableTimestampDelta, "maximum acceptable time difference between the local time and the latest L1 block's timestamp") + f.StringArray(prefix+".sender-whitelist", DefaultSequencerConfig.SenderWhitelist, "whitelist of authorized senders (if empty, everyone is allowed)") DangerousSequencerConfigAddOptions(prefix+".dangerous", f) } diff --git a/arbnode/sequencer.go b/arbnode/sequencer.go index 89edd22935..e1c38ec2ab 100644 --- a/arbnode/sequencer.go +++ b/arbnode/sequencer.go @@ -42,10 +42,11 @@ func (i *txQueueItem) returnResult(err error) { type Sequencer struct { stopwaiter.StopWaiter - txStreamer *TransactionStreamer - txQueue chan txQueueItem - l1Reader *headerreader.HeaderReader - config SequencerConfig + txStreamer *TransactionStreamer + txQueue chan txQueueItem + l1Reader *headerreader.HeaderReader + config SequencerConfig + senderWhitelist map[common.Address]struct{} L1BlockAndTimeMutex sync.Mutex l1BlockNumber uint64 @@ -56,17 +57,40 @@ type Sequencer struct { } func NewSequencer(txStreamer *TransactionStreamer, l1Reader *headerreader.HeaderReader, config SequencerConfig) (*Sequencer, error) { + senderWhitelist := make(map[common.Address]struct{}) + for _, address := range config.SenderWhitelist { + if len(address) == 0 { + continue + } + if !common.IsHexAddress(address) { + return nil, fmt.Errorf("sequencer sender whitelist entry \"%v\" is not a valid address", address) + } + senderWhitelist[common.HexToAddress(address)] = struct{}{} + } return &Sequencer{ - txStreamer: txStreamer, - txQueue: make(chan txQueueItem, 128), - l1Reader: l1Reader, - config: config, - l1BlockNumber: 0, - l1Timestamp: 0, + txStreamer: txStreamer, + txQueue: make(chan txQueueItem, 128), + l1Reader: l1Reader, + config: config, + senderWhitelist: senderWhitelist, + l1BlockNumber: 0, + l1Timestamp: 0, }, nil } func (s *Sequencer) PublishTransaction(ctx context.Context, tx *types.Transaction) error { + if len(s.senderWhitelist) > 0 { + signer := types.LatestSigner(s.txStreamer.bc.Config()) + sender, err := types.Sender(signer, tx) + if err != nil { + return err + } + _, authorized := s.senderWhitelist[sender] + if !authorized { + return errors.New("transaction sender is not on the whitelist") + } + } + resultChan := make(chan error, 1) queueItem := txQueueItem{ tx, diff --git a/system_tests/seq_whitelist_test.go b/system_tests/seq_whitelist_test.go new file mode 100644 index 0000000000..7cfdf1caca --- /dev/null +++ b/system_tests/seq_whitelist_test.go @@ -0,0 +1,42 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package arbtest + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbnode" +) + +func TestSequencerWhitelist(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + config := arbnode.ConfigDefaultL2Test() + config.Sequencer.SenderWhitelist = []string{ + GetTestAddressForAccountName(t, "Owner").String(), + GetTestAddressForAccountName(t, "User").String(), + } + l2info, _, client := CreateTestL2WithConfig(t, ctx, nil, config, true) + + l2info.GenerateAccount("User") + l2info.GenerateAccount("User2") + + // Owner is on the whitelist + TransferBalance(t, "Owner", "User", big.NewInt(params.Ether), l2info, client, ctx) + TransferBalance(t, "Owner", "User2", big.NewInt(params.Ether), l2info, client, ctx) + + // User is on the whitelist + TransferBalance(t, "User", "User2", big.NewInt(params.Ether/10), l2info, client, ctx) + + // User2 is *not* on the whitelist, therefore this should fail + tx := l2info.PrepareTx("User2", "User", l2info.TransferGas, big.NewInt(params.Ether/10), nil) + err := client.SendTransaction(ctx, tx) + if err == nil { + Fail(t, "transaction from user not on whitelist accepted") + } +} diff --git a/system_tests/test_info.go b/system_tests/test_info.go index 48b5824973..ef838344a5 100644 --- a/system_tests/test_info.go +++ b/system_tests/test_info.go @@ -64,9 +64,7 @@ func NewL1TestInfo(t *testing.T) *BlockchainTestInfo { return NewBlockChainTestInfo(t, types.NewLondonSigner(simulatedChainID), big.NewInt(params.GWei*100), params.TxGas) } -func (b *BlockchainTestInfo) GenerateAccount(name string) { - b.T.Helper() - +func GetTestKeyForAccountName(t *testing.T, name string) *ecdsa.PrivateKey { nameBytes := []byte(name) seedBytes := make([]byte, 0, 128) for len(seedBytes) < 64 { @@ -75,8 +73,20 @@ func (b *BlockchainTestInfo) GenerateAccount(name string) { seedReader := bytes.NewReader(seedBytes) privateKey, err := ecdsa.GenerateKey(crypto.S256(), seedReader) if err != nil { - b.T.Fatal(err) + t.Fatal(err) } + return privateKey +} + +func GetTestAddressForAccountName(t *testing.T, name string) common.Address { + privateKey := GetTestKeyForAccountName(t, name) + return crypto.PubkeyToAddress(privateKey.PublicKey) +} + +func (b *BlockchainTestInfo) GenerateAccount(name string) { + b.T.Helper() + + privateKey := GetTestKeyForAccountName(b.T, name) if b.Accounts[name] != nil { b.T.Fatal("account already exists") }