diff --git a/consensus/system_contract/system_contract_test.go b/consensus/system_contract/system_contract_test.go new file mode 100644 index 000000000000..36edf93aed93 --- /dev/null +++ b/consensus/system_contract/system_contract_test.go @@ -0,0 +1,250 @@ +package system_contract + +import ( + "context" + "github.com/scroll-tech/go-ethereum" + "github.com/scroll-tech/go-ethereum/accounts" + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/log" + "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/rollup/sync_service" + "github.com/scroll-tech/go-ethereum/trie" + "github.com/stretchr/testify/require" + "math/big" + "sync" + "testing" + "time" +) + +var _ sync_service.EthClient = &fakeEthClient{} + +func TestSystemContract_FetchSigner(t *testing.T) { + // Turn off logging for tests to keep output clean. + log.Root().SetHandler(log.DiscardHandler()) + + // Expected signer address returned by our fake EthClient. + expectedSigner := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + + // Create our fake client to always return expectedSigner. + fakeClient := &fakeEthClient{value: expectedSigner} + + config := ¶ms.SystemContractConfig{ + SystemContractAddress: common.HexToAddress("0xFAKE"), + // The slot number can be arbitrary – fake client doesn't use it. + Period: 10, + RelaxedPeriod: false, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Create a new SystemContract instance. + sys := New(ctx, config, fakeClient) + defer sys.Close() + + // Since the SystemContract's Start() routine fetches and updates s.signerAddressL1 + // in a separate goroutine, wait a bit for that to complete. + time.Sleep(2 * time.Second) + + // Acquire a read lock to safely read the value. + sys.lock.RLock() + actualSigner := sys.signerAddressL1 + sys.lock.RUnlock() + + // Verify that the fetched signer equals the expectedSigner from our fake client. + require.Equal(t, expectedSigner, actualSigner, "The SystemContract should update signerAddressL1 to the value provided by the client") +} + +func TestSystemContract_AuthorizeCheck(t *testing.T) { + // This test verifies that if the local signer does not match the authorized signer, + // then the Seal() function returns an error. + + // Set the expected signer. + expectedSigner := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + + fakeClient := &fakeEthClient{value: expectedSigner} + config := ¶ms.SystemContractConfig{ + SystemContractAddress: common.HexToAddress("0xFAKE"), + Period: 10, + RelaxedPeriod: false, + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + sys := New(ctx, config, fakeClient) + defer sys.Close() + + // Wait to ensure that the background routine has updated signerAddressL1. + time.Sleep(2 * time.Second) + + // Authorize with a different signer than expected. + differentSigner := common.HexToAddress("0xABCDEFabcdefABCDEFabcdefabcdefABCDEFABCD") + sys.Authorize(differentSigner, func(acc accounts.Account, mimeType string, message []byte) ([]byte, error) { + // For testing, return a dummy signature + return []byte("dummy_sig"), nil + }) + + // Create a dummy block header (with dummy values). + // We only need the block number and extra data length for this test. + header := &types.Header{ + Number: big.NewInt(100), + // We use an extra slice with length equal to extraSeal + Extra: make([]byte, extraSeal), + // Set other fields minimally for the test: + Time: uint64(time.Now().Unix() + 10), + GasLimit: 10000000, + Nonce: types.BlockNonce{}, // zero nonce as required + MixDigest: common.Hash{}, // dummy; in a PoA system this should be zero + Difficulty: big.NewInt(1), + } + + // Call Seal() and expect an error since local signer != authorized signer. + results := make(chan *types.Block) + stop := make(chan struct{}) + err := sys.Seal(nil, (&types.Block{}).WithSeal(header), results, stop) + + require.Error(t, err, "Seal should return an error when the local signer is not authorized") +} + +// TestSystemContract_SignsAfterUpdate simulates: +// 1. Initially, the SystemContract authorized signer (from StorageAt) is not the signer of the Block. +// 2. Later, after updating the fake client to the correct signer, the background +// poll updates the SystemContract. +// 3. Once updated, if the local signing key is set to match, Seal() should succeed. +func TestSystemContract_SignsAfterUpdate(t *testing.T) { + // Silence logging during tests. + log.Root().SetHandler(log.DiscardHandler()) + + // Define two addresses: one "wrong" and one "correct". + oldSigner := common.HexToAddress("0x1111111111111111111111111111111111111111") + updatedSigner := common.HexToAddress("0x2222222222222222222222222222222222222222") + + // Create a fake client that starts by returning the wrong signer. + fakeClient := &fakeEthClient{ + value: oldSigner, + } + + config := ¶ms.SystemContractConfig{ + SystemContractAddress: common.HexToAddress("0xFAKE"), // Dummy value + Period: 10, // arbitrary non-zero value + RelaxedPeriod: false, + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // Create the SystemContract with our fake client. + sys := New(ctx, config, fakeClient) + defer sys.Close() + + // Wait for the background routine to poll at least once. + time.Sleep(2 * time.Second) + + // Verify that initially the fetched signer equals oldSigner. + sys.lock.RLock() + initialSigner := sys.signerAddressL1 + sys.lock.RUnlock() + require.Equal(t, oldSigner, initialSigner, "Initial signerAddressL1 should be oldSigner") + + // Now, simulate an update: change the fake client's returned value to updatedSigner. + fakeClient.mu.Lock() + fakeClient.value = updatedSigner + fakeClient.mu.Unlock() + + // Wait enough for the background polling routine to fetch the new value. + time.Sleep(1 + time.Second + defaultSyncInterval) + + // Verify that system contract's signerAddressL1 is now updated to updatedSigner. + sys.lock.RLock() + newSigner := sys.signerAddressL1 + sys.lock.RUnlock() + require.Equal(t, newSigner, updatedSigner, "SignerAddressL1 should update to updatedSigner after polling") + + // Now simulate authorizing with the correct local signer. + sys.Authorize(updatedSigner, func(acc accounts.Account, mimeType string, message []byte) ([]byte, error) { + // For testing, return a dummy signature. + return []byte("dummy_signature"), nil + }) + + // Create a dummy header for sealing. + header := &types.Header{ + Number: big.NewInt(100), + Extra: make([]byte, extraSeal), + Time: uint64(time.Now().Add(10 * time.Second).Unix()), + GasLimit: 10000000, + Nonce: types.BlockNonce{}, // must be zero as per rules + MixDigest: common.Hash{}, // for PoA, typically zero + Difficulty: big.NewInt(1), + } + + // Construct a new block from the header using NewBlock constructor. + block := types.NewBlock(header, nil, nil, nil, trie.NewStackTrie(nil)) + + // Set up channels required for Seal. + results := make(chan *types.Block) + stop := make(chan struct{}) + + // Call Seal. It should succeed (i.e. return no error) because local signer now equals authorized signer. + err := sys.Seal(nil, block, results, stop) + require.NoError(t, err, "Seal should succeed when the local signer is authorized after update") + + // Wait for the result from Seal's goroutine. + select { + case sealedBlock := <-results: + require.NotNil(t, sealedBlock, "Seal should eventually return a sealed block") + // Optionally, you may log or further inspect sealedBlock here. + case <-time.After(15 * time.Second): + t.Fatal("Timed out waiting for Seal to return a sealed block") + } +} + +// fakeEthClient implements a minimal version of sync_service.EthClient for testing purposes. +type fakeEthClient struct { + mu sync.Mutex + // value is the fixed value to return from StorageAt. + // We'll assume StorageAt returns a 32-byte value representing an Ethereum address. + value common.Address +} + +// BlockNumber returns 0. +func (f *fakeEthClient) BlockNumber(ctx context.Context) (uint64, error) { + return 0, nil +} + +// ChainID returns a zero-value chain ID. +func (f *fakeEthClient) ChainID(ctx context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} + +// FilterLogs returns an empty slice of logs. +func (f *fakeEthClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { + return []types.Log{}, nil +} + +// HeaderByNumber returns nil. +func (f *fakeEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + return nil, nil +} + +// SubscribeFilterLogs returns a nil subscription. +func (f *fakeEthClient) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + return nil, nil +} + +// TransactionByHash returns (nil, false, nil). +func (f *fakeEthClient) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) { + return nil, false, nil +} + +// BlockByHash returns nil. +func (f *fakeEthClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + return nil, nil +} + +// StorageAt returns the byte representation of f.value. +func (f *fakeEthClient) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { + f.mu.Lock() + defer f.mu.Unlock() + return f.value.Bytes(), nil +} diff --git a/go.mod b/go.mod index db0e122a1d37..3ffa21e58fdc 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/edsrzf/mmap-go v1.0.0 github.com/ethereum/c-kzg-4844/bindings/go v0.0.0-20230126171313-363c7d7593b4 github.com/fatih/color v1.7.0 - github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 + github.com/fjl/memsize v0.0.2 github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff github.com/go-stack/stack v1.8.1 github.com/golang/protobuf v1.4.3 diff --git a/go.sum b/go.sum index 47f631beb92e..2f6680439bd9 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,8 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA= +github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= diff --git a/rollup/rollup_sync_service/l1client_test.go b/rollup/rollup_sync_service/l1client_test.go index 8a42bfb3068e..397e25a29ade 100644 --- a/rollup/rollup_sync_service/l1client_test.go +++ b/rollup/rollup_sync_service/l1client_test.go @@ -76,4 +76,3 @@ func (m *mockEthClient) BlockByHash(ctx context.Context, hash common.Hash) (*typ func (m *mockEthClient) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { return nil, nil } -