diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 6290347b4e3e..970919215d09 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -254,7 +254,7 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*enode.Node, network ui cfg.Genesis = genesis utils.SetDNSDiscoveryDefaults(&cfg, genesis.ToBlock(nil).Hash()) - lesBackend, err := les.New(stack, &cfg) + lesBackend, err := les.New(stack, &cfg, nil) if err != nil { return nil, fmt.Errorf("Failed to register the Ethereum service: %w", err) } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 7bc8fe5d70c0..8d8bdf6773fc 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -2048,16 +2048,6 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) { // The second return value is the full node instance, which may be nil if the // node is running as a light client. func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) { - if cfg.SyncMode == downloader.LightSync { - backend, err := les.New(stack, cfg) - if err != nil { - Fatalf("Failed to register the Ethereum service: %v", err) - } - scrollTracerWrapper := tracing.NewTracerWrapper() - stack.RegisterAPIs(tracers.APIs(backend.ApiBackend, scrollTracerWrapper)) - return backend.ApiBackend, nil - } - // initialize L1 client for sync service // note: we need to do this here to avoid circular dependency l1EndpointUrl := stack.Config().L1Endpoint @@ -2073,6 +2063,16 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend log.Info("Initialized L1 client", "endpoint", l1EndpointUrl) } + if cfg.SyncMode == downloader.LightSync { + backend, err := les.New(stack, cfg, l1Client) + if err != nil { + Fatalf("Failed to register the Ethereum service: %v", err) + } + scrollTracerWrapper := tracing.NewTracerWrapper() + stack.RegisterAPIs(tracers.APIs(backend.ApiBackend, scrollTracerWrapper)) + return backend.ApiBackend, nil + } + backend, err := eth.New(stack, cfg, l1Client) if err != nil { Fatalf("Failed to register the Ethereum service: %v", err) diff --git a/consensus/system_contract/api.go b/consensus/system_contract/api.go new file mode 100644 index 000000000000..c975347f1b99 --- /dev/null +++ b/consensus/system_contract/api.go @@ -0,0 +1,18 @@ +package system_contract + +import ( + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/consensus" + "github.com/scroll-tech/go-ethereum/rpc" +) + +// API is a user facing RPC API to allow controlling the signer and voting +// mechanisms of the proof-of-authority scheme. +type API struct { + chain consensus.ChainHeaderReader +} + +// GetSigners retrieves the list of authorized signers at the specified block. +func (api *API) GetSigners(number *rpc.BlockNumber) ([]common.Address, error) { + return nil, nil +} diff --git a/consensus/system_contract/consensus.go b/consensus/system_contract/consensus.go new file mode 100644 index 000000000000..456cf0d7ef42 --- /dev/null +++ b/consensus/system_contract/consensus.go @@ -0,0 +1,388 @@ +package system_contract + +import ( + "bytes" + "errors" + "fmt" + "io" + "math/big" + "time" + + "golang.org/x/crypto/sha3" + + "github.com/scroll-tech/go-ethereum/accounts" + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/consensus" + "github.com/scroll-tech/go-ethereum/consensus/misc" + "github.com/scroll-tech/go-ethereum/core/state" + "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/rlp" + "github.com/scroll-tech/go-ethereum/rpc" + "github.com/scroll-tech/go-ethereum/trie" +) + +var ( + extraSeal = crypto.SignatureLength // Fixed number of extra-data suffix bytes reserved for signer seal + uncleHash = types.CalcUncleHash(nil) // Always Keccak256(RLP([])) as uncles are meaningless outside of PoW. +) + +// Various error messages to mark blocks invalid. These should be private to +// prevent engine specific errors from being referenced in the remainder of the +// codebase, inherently breaking if the engine is swapped out. Please put common +// error types into the consensus package. +var ( + // errUnknownBlock is returned when the list of signers is requested for a block + // that is not part of the local blockchain. + errUnknownBlock = errors.New("unknown block") + // errNonceNotEmpty is returned if a nonce value is non-zero + errInvalidNonce = errors.New("nonce not empty nor zero") + // errMissingSignature is returned if a block's extra-data section doesn't seem + // to contain a 65 byte secp256k1 signature. + errMissingSignature = errors.New("extra-data 65 byte signature missing") + // errInvalidMixDigest is returned if a block's mix digest is non-zero. + errInvalidMixDigest = errors.New("non-zero mix digest") + // errInvalidUncleHash is returned if a block contains an non-empty uncle list. + errInvalidUncleHash = errors.New("non empty uncle hash") + // errInvalidDifficulty is returned if a difficulty value is non-zero + errInvalidDifficulty = errors.New("non-one difficulty") + // errInvalidTimestamp is returned if the timestamp of a block is lower than + // the previous block's timestamp + the minimum block period. + errInvalidTimestamp = errors.New("invalid timestamp") + // errInvalidExtra is returned if the extra data is not empty + errInvalidExtra = errors.New("invalid extra") +) + +// ErrUnauthorizedSigner is returned if a header is signed by a non-authorized entity. +var ErrUnauthorizedSigner = errors.New("unauthorized signer") + +// SignerFn hashes and signs the data to be signed by a backing account. +type SignerFn func(signer accounts.Account, mimeType string, message []byte) ([]byte, error) + +// Author implements consensus.Engine, returning the Ethereum address recovered +// from the signature in the header's block-signature section. +func (s *SystemContract) Author(header *types.Header) (common.Address, error) { + return ecrecover(header) +} + +// VerifyHeader checks whether a header conforms to the consensus rules of a +// given engine. +func (s *SystemContract) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header, seal bool) error { + return s.verifyHeader(chain, header, nil) +} + +// VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers +// concurrently. The method returns a quit channel to abort the operations and +// a results channel to retrieve the async verifications (the order is that of +// the input slice). +func (s *SystemContract) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { + abort := make(chan struct{}) + results := make(chan error, len(headers)) + + go func() { + for i, header := range headers { + err := s.verifyHeader(chain, header, headers[:i]) + if err != nil { + log.Error("Error verifying headers", "err", err) + } + select { + case <-abort: + return + case results <- err: + } + } + }() + return abort, results +} + +// verifyHeader checks whether a header conforms to the consensus rules.The +// caller may optionally pass in a batch of parents (ascending order) to avoid +// looking those up from the database. This is useful for concurrently verifying +// a batch of new headers. +func (s *SystemContract) verifyHeader(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + if header.Number == nil { + return errUnknownBlock + } + + // Don't waste time checking blocks from the future + if header.Time > uint64(time.Now().Unix()) { + return consensus.ErrFutureBlock + } + // Ensure that the nonce is zero + if header.Nonce != (types.BlockNonce{}) { + return errInvalidNonce + } + // Check that the BlockSignature contains signature if block is not requested + if !header.Requested && header.Number.Cmp(big.NewInt(0)) != 0 && len(header.BlockSignature) != extraSeal { + return errMissingSignature + } + // Ensure that the mix digest is zero + if header.MixDigest != (common.Hash{}) { + return errInvalidMixDigest + } + // Ensure that the block doesn't contain any uncles which are meaningless in PoA + if header.UncleHash != uncleHash { + return errInvalidUncleHash + } + // Ensure that the difficulty is one + if header.Difficulty.Cmp(common.Big1) != 0 { + return errInvalidDifficulty + } + // Verify that the gas limit is <= 2^63-1 + cap := uint64(0x7fffffffffffffff) + if header.GasLimit > cap { + return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap) + } + if len(header.Extra) > 0 { + return errInvalidExtra + } + //// All basic checks passed, verify cascading fields + return s.verifyCascadingFields(chain, header, parents) +} + +// verifyCascadingFields verifies all the header fields that are not standalone, +// rather depend on a batch of previous headers. The caller may optionally pass +// in a batch of parents (ascending order) to avoid looking those up from the +// database. This is useful for concurrently verifying a batch of new headers. +func (s *SystemContract) verifyCascadingFields(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + // The genesis block is the always valid dead-end + number := header.Number.Uint64() + if number == 0 { + return nil + } + // Ensure that the block's timestamp isn't too close to its parent + var parent *types.Header + if len(parents) > 0 { + parent = parents[len(parents)-1] + } else { + parent = chain.GetHeader(header.ParentHash, number-1) + } + if parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash { + return consensus.ErrUnknownAncestor + } + if header.Time < parent.Time { + return errInvalidTimestamp + } + // Verify that the gasUsed is <= gasLimit + if header.GasUsed > header.GasLimit { + return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit) + } + if !chain.Config().IsCurie(header.Number) { + // Verify BaseFee not present before EIP-1559 fork. + if header.BaseFee != nil { + return fmt.Errorf("invalid baseFee before fork: have %d, want ", header.BaseFee) + } + if err := misc.VerifyGaslimit(parent.GasLimit, header.GasLimit); err != nil { + return err + } + } else if err := misc.VerifyEip1559Header(chain.Config(), parent, header); err != nil { + // Verify the header's EIP-1559 attributes. + return err + } + + // only if block header has NOT been requested, then verify the signature against the current signer + if !header.Requested { + signer, err := ecrecover(header) + if err != nil { + return err + } + + s.lock.RLock() + defer s.lock.RUnlock() + + if signer != s.signerAddressL1 { + log.Error("Unauthorized signer", "Got", signer, "Expected:", s.signerAddressL1) + return ErrUnauthorizedSigner + } + } + + return nil +} + +// VerifyUncles implements consensus.Engine, always returning an error for any +// uncles as this consensus mechanism doesn't permit uncles. +func (s *SystemContract) VerifyUncles(chain consensus.ChainReader, block *types.Block) error { + if len(block.Uncles()) > 0 { + return errors.New("uncles not allowed") + } + return nil +} + +// Prepare initializes the consensus fields of a block header according to the +// rules of a particular engine. Update only timestamp and prepare ExtraData for Signature +func (s *SystemContract) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { + header.BlockSignature = make([]byte, extraSeal) + header.IsEuclidV2 = true + header.Extra = nil + // Ensure the timestamp has the correct delay + parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) + if parent == nil { + return consensus.ErrUnknownAncestor + } + header.Time = parent.Time + s.config.Period + // If RelaxedPeriod is enabled, always set the header timestamp to now (ie the time we start building it) as + // we don't know when it will be sealed + if s.config.RelaxedPeriod || header.Time < uint64(time.Now().Unix()) { + header.Time = uint64(time.Now().Unix()) + } + header.Difficulty = big.NewInt(1) + return nil +} + +// Finalize implements consensus.Engine. There is no post-transaction +// No rules here +func (s *SystemContract) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) { + // No block rewards in PoA, so the state remains as is +} + +// FinalizeAndAssemble implements consensus.Engine, ensuring no uncles are set, +// nor block rewards given, and returns the final block. +func (s *SystemContract) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) { + // Finalize block + s.Finalize(chain, header, state, txs, uncles) + + // Assign the final state root to header. + header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) + + // Assemble and return the final block for sealing. + return types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)), nil +} + +// Seal implements consensus.Engine, attempting to create a sealed block using +// the local signing credentials. +func (s *SystemContract) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { + header := block.Header() + // Sealing the genesis block is not supported + number := header.Number.Uint64() + if number == 0 { + return errUnknownBlock + } + + // For 0-period chains, refuse to seal empty blocks (no reward but would spin sealing) + if s.config.Period == 0 && len(block.Transactions()) == 0 { + return errors.New("sealing paused while waiting for transactions") + } + + // Don't hold the signer fields for the entire sealing procedure + s.lock.RLock() + signer, signFn := s.signer, s.signFn + signerAddressL1 := s.signerAddressL1 + s.lock.RUnlock() + + // Bail out if we are unauthorized to sign a block + if signer != signerAddressL1 { + return ErrUnauthorizedSigner + } + + // Sweet, the protocol permits us to sign the block, wait for our time + delay := time.Unix(int64(header.Time), 0).Sub(time.Now()) // nolint: gosimple + + // Sign all the things! + sighash, err := signFn(accounts.Account{Address: signer}, accounts.MimetypeClique, SystemContractRLP(header)) + if err != nil { + return err + } + copy(header.BlockSignature[0:], sighash) + // Wait until sealing is terminated or delay timeout. + log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay)) + go func() { + defer close(results) + + select { + case <-stop: + return + case <-time.After(delay): + } + + select { + case results <- block.WithSeal(header): + case <-time.After(time.Second): + log.Warn("Sealing result is not read by miner", "sealhash", SealHash(header)) + } + }() + + return nil +} + +// SealHash returns the hash of a block prior to it being sealed. +func (s *SystemContract) SealHash(header *types.Header) (hash common.Hash) { + return SealHash(header) +} + +// SealHash returns the hash of a block prior to it being sealed. +func SealHash(header *types.Header) (hash common.Hash) { + hasher := sha3.NewLegacyKeccak256() + encodeSigHeader(hasher, header) + hasher.(crypto.KeccakState).Read(hash[:]) + return hash +} + +// ecrecover extracts the Ethereum account address from a signed header. +func ecrecover(header *types.Header) (common.Address, error) { + signature := header.BlockSignature[0:] + + // Recover the public key and the Ethereum address + pubkey, err := crypto.Ecrecover(SealHash(header).Bytes(), signature) + if err != nil { + return common.Address{}, err + } + var signer common.Address + copy(signer[:], crypto.Keccak256(pubkey[1:])[12:]) + + return signer, nil +} + +// SystemContractRLP returns the rlp bytes which needs to be signed for the system contract +// sealing. The RLP to sign consists of the entire header apart from the ExtraData +// +// Note, the method requires the extra data to be at least 65 bytes, otherwise it +// panics. This is done to avoid accidentally using both forms (signature present +// or not), which could be abused to produce different hashes for the same header. +func SystemContractRLP(header *types.Header) []byte { + b := new(bytes.Buffer) + encodeSigHeader(b, header) + return b.Bytes() +} + +// CalcDifficulty implements consensus.Engine. There is no difficulty rules here +func (s *SystemContract) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { + return nil +} + +// APIs implements consensus.Engine, returning the user facing RPC API to allow +// controlling the signer voting. +func (s *SystemContract) APIs(chain consensus.ChainHeaderReader) []rpc.API { + return []rpc.API{{ + Namespace: "system_contract", + Service: &API{}, + }} +} + +func encodeSigHeader(w io.Writer, header *types.Header) { + enc := []interface{}{ + header.ParentHash, + header.UncleHash, + header.Coinbase, + header.Root, + header.TxHash, + header.ReceiptHash, + header.Bloom, + header.Difficulty, + header.Number, + header.GasLimit, + header.GasUsed, + header.Time, + header.MixDigest, + header.Nonce, + } + if header.BaseFee != nil { + enc = append(enc, header.BaseFee) + } + if header.WithdrawalsHash != nil { + panic("unexpected withdrawal hash value") + } + if err := rlp.Encode(w, enc); err != nil { + panic("can't encode: " + err.Error()) + } +} diff --git a/consensus/system_contract/system_contract.go b/consensus/system_contract/system_contract.go new file mode 100644 index 000000000000..ebd753661788 --- /dev/null +++ b/consensus/system_contract/system_contract.go @@ -0,0 +1,125 @@ +package system_contract + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/log" + "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/rollup/sync_service" +) + +const ( + defaultSyncInterval = 10 * time.Second +) + +// SystemContract +type SystemContract struct { + config *params.SystemContractConfig // Consensus engine configuration parameters + client sync_service.EthClient + + signerAddressL1 common.Address // Address of the signer stored in L1 System Contract + + signer common.Address // Ethereum address of the signing key + signFn SignerFn // Signer function to authorize hashes with + lock sync.RWMutex // Protects the signer and proposals fields + + ctx context.Context + cancel context.CancelFunc +} + +// New creates a SystemContract consensus engine with the initial +// signers set to the ones provided by the user. +func New(ctx context.Context, config *params.SystemContractConfig, client sync_service.EthClient) *SystemContract { + ctx, cancel := context.WithCancel(ctx) + + s := &SystemContract{ + config: config, + client: client, + + ctx: ctx, + cancel: cancel, + } + + if err := s.fetchAddressFromL1(); err != nil { + log.Error("failed to fetch signer address from L1", "err", err) + } + return s +} + +// Authorize injects a private key into the consensus engine to mint new blocks +// with. +func (s *SystemContract) Authorize(signer common.Address, signFn SignerFn) { + log.Info("Authorizing system contract", "signer", signer.Hex()) + s.lock.Lock() + defer s.lock.Unlock() + + s.signer = signer + s.signFn = signFn +} + +func (s *SystemContract) Start() { + go func() { + log.Info("starting SystemContract") + syncTicker := time.NewTicker(defaultSyncInterval) + defer syncTicker.Stop() + for { + select { + case <-s.ctx.Done(): + return + default: + } + select { + case <-s.ctx.Done(): + return + case <-syncTicker.C: + if err := s.fetchAddressFromL1(); err != nil { + log.Error("failed to fetch signer address from L1", "err", err) + } + } + } + }() +} + +func (s *SystemContract) fetchAddressFromL1() error { + address, err := s.client.StorageAt(s.ctx, s.config.SystemContractAddress, s.config.SystemContractSlot, nil) + if err != nil { + return fmt.Errorf("failed to get signer address from L1 System Contract: %w", err) + } + bAddress := common.BytesToAddress(address) + + // Validate the address is not empty + if bAddress == (common.Address{}) { + return fmt.Errorf("retrieved empty signer address from L1 System Contract") + } + + log.Debug("Read address from system contract", "address", bAddress.Hex()) + + s.lock.RLock() + addressChanged := s.signerAddressL1 != bAddress + s.lock.RUnlock() + + if addressChanged { + s.lock.Lock() + s.signerAddressL1 = bAddress + s.lock.Unlock() + } + + return nil +} + +// Close implements consensus.Engine. +func (s *SystemContract) Close() error { + s.cancel() + return nil +} + +func (s *SystemContract) currentSignerAddressL1() common.Address { + s.lock.RLock() + defer s.lock.RUnlock() + + return s.signerAddressL1 +} diff --git a/consensus/system_contract/system_contract_test.go b/consensus/system_contract/system_contract_test.go new file mode 100644 index 000000000000..ddd6a0fc8d66 --- /dev/null +++ b/consensus/system_contract/system_contract_test.go @@ -0,0 +1,223 @@ +package system_contract + +import ( + "context" + "math/big" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "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" +) + +var _ sync_service.EthClient = &fakeEthClient{} + +func TestSystemContract_FetchSigner(t *testing.T) { + log.Root().SetHandler(log.DiscardHandler()) + + expectedSigner := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + + 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() + + sys := New(ctx, config, fakeClient) + defer sys.Close() + + require.NoError(t, sys.fetchAddressFromL1()) + + actualSigner := sys.currentSignerAddressL1() + + // 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. + + 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() + + require.NoError(t, sys.fetchAddressFromL1()) + + // 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. + // We only need the block number and blocksignature data length for this test. + header := &types.Header{ + Number: big.NewInt(100), + // We use an extra slice with length equal to extraSeal + BlockSignature: make([]byte, extraSeal), + } + + // 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() + + sys := New(ctx, config, fakeClient) + defer sys.Close() + + require.NoError(t, sys.fetchAddressFromL1()) + + // Verify that initially the fetched signer equals oldSigner. + initialSigner := sys.currentSignerAddressL1() + 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() + + // fetch new value from L1 (simulating a background poll) + require.NoError(t, sys.fetchAddressFromL1()) + + // Verify that system contract's signerAddressL1 is now updated to updatedSigner. + newSigner := sys.currentSignerAddressL1() + 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), + BlockSignature: make([]byte, extraSeal), + } + + // Construct a new block from the header using NewBlock constructor. + block := types.NewBlock(header, nil, nil, nil, trie.NewStackTrie(nil)) + + 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/consensus/wrapper/consensus.go b/consensus/wrapper/consensus.go new file mode 100644 index 000000000000..040de0ef467f --- /dev/null +++ b/consensus/wrapper/consensus.go @@ -0,0 +1,207 @@ +package wrapper + +import ( + "math/big" + "sync" + + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/consensus" + "github.com/scroll-tech/go-ethereum/consensus/clique" + "github.com/scroll-tech/go-ethereum/consensus/system_contract" + "github.com/scroll-tech/go-ethereum/core/state" + "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/rpc" +) + +// UpgradableEngine implements consensus.Engine and acts as a middleware to dispatch +// calls to either Clique or SystemContract consensus. +type UpgradableEngine struct { + // forkBlock is the block number at which the switchover to SystemContract occurs. + isUpgraded func(uint64) bool + + // clique is the original Clique consensus engine. + clique consensus.Engine + + // system is the new SystemContract consensus engine. + system consensus.Engine +} + +// NewUpgradableEngine constructs a new upgradable consensus middleware. +func NewUpgradableEngine(isUpgraded func(uint64) bool, clique consensus.Engine, system consensus.Engine) *UpgradableEngine { + return &UpgradableEngine{ + isUpgraded: isUpgraded, + clique: clique, + system: system, + } +} + +// chooseEngine returns the appropriate consensus engine based on the header's number. +func (ue *UpgradableEngine) chooseEngine(header *types.Header) consensus.Engine { + if ue.isUpgraded(header.Time) { + return ue.system + } + return ue.clique +} + +// -------------------- +// Methods to implement consensus.Engine + +// Author returns the author of the block based on the header. +func (ue *UpgradableEngine) Author(header *types.Header) (common.Address, error) { + return ue.chooseEngine(header).Author(header) +} + +// VerifyHeader checks whether a header conforms to the consensus rules of the engine. +func (ue *UpgradableEngine) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header, seal bool) error { + return ue.chooseEngine(header).VerifyHeader(chain, header, seal) +} + +// VerifyHeaders verifies a batch of headers concurrently. In our use-case, +// headers can only be all system, all clique, or start with clique and then switch once to system. +func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { + abort := make(chan struct{}) + out := make(chan error) + + // If there are no headers, return a closed error channel. + if len(headers) == 0 { + close(out) + return nil, out + } + + // Choose engine for the first and last header. + firstEngine := ue.chooseEngine(headers[0]) + lastEngine := ue.chooseEngine(headers[len(headers)-1]) + + // If the first header is system, then all headers must be system. + if firstEngine == ue.system { + return firstEngine.VerifyHeaders(chain, headers, seals) + } + + // If first and last headers are both clique, then all headers are clique. + if firstEngine == lastEngine { + return firstEngine.VerifyHeaders(chain, headers, seals) + } + + // Otherwise, headers start as clique then switch to system. Since we assume + // a single switchover, find the first header that uses system. + splitIndex := 0 + for i, header := range headers { + if ue.chooseEngine(header) == ue.system { + splitIndex = i + break + } + } + // It's expected that splitIndex is > 0. + cliqueHeaders := headers[:splitIndex] + cliqueSeals := seals[:splitIndex] + systemHeaders := headers[splitIndex:] + systemSeals := seals[splitIndex:] + + // Create a wait group to merge results. + var wg sync.WaitGroup + wg.Add(2) + + // Launch concurrent verifications. + go func() { + defer wg.Done() + _, cliqueResults := ue.clique.VerifyHeaders(chain, cliqueHeaders, cliqueSeals) + for err := range cliqueResults { + select { + case <-abort: + return + case out <- err: + } + } + }() + + go func() { + defer wg.Done() + _, systemResults := ue.system.VerifyHeaders(chain, systemHeaders, systemSeals) + for err := range systemResults { + select { + case <-abort: + return + case out <- err: + } + } + }() + + // Close the out channel when both verifications are complete. + go func() { + wg.Wait() + close(out) + }() + + return abort, out +} + +// Prepare prepares a block header for sealing. +func (ue *UpgradableEngine) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { + return ue.chooseEngine(header).Prepare(chain, header) +} + +// Seal instructs the engine to start sealing a block. +func (ue *UpgradableEngine) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { + return ue.chooseEngine(block.Header()).Seal(chain, block, results, stop) +} + +// CalcDifficulty calculates the block difficulty if applicable. +func (ue *UpgradableEngine) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { + return ue.chooseEngine(parent).CalcDifficulty(chain, time, parent) +} + +// Finalize finalizes the block, applying any post-transaction rules. +func (ue *UpgradableEngine) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) { + ue.chooseEngine(header).Finalize(chain, header, state, txs, uncles) +} + +// FinalizeAndAssemble finalizes and assembles a new block. +func (ue *UpgradableEngine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) { + return ue.chooseEngine(header).FinalizeAndAssemble(chain, header, state, txs, uncles, receipts) +} + +// VerifyUncles verifies that no uncles are attached to the block. +func (ue *UpgradableEngine) VerifyUncles(chain consensus.ChainReader, block *types.Block) error { + return ue.chooseEngine(block.Header()).VerifyUncles(chain, block) +} + +// APIs returns any RPC APIs exposed by the consensus engine. +func (ue *UpgradableEngine) APIs(chain consensus.ChainHeaderReader) []rpc.API { + // Determine the current chain head. + head := chain.CurrentHeader() + if head == nil { + // Fallback: return the clique APIs (or an empty slice) if we don't have a header. + return ue.clique.APIs(chain) + } + + // Choose engine based on whether the chain head is before or after the fork block. + if ue.isUpgraded(head.Time) { + return ue.system.APIs(chain) + } + return ue.clique.APIs(chain) +} + +// Close terminates the consensus engine. +func (ue *UpgradableEngine) Close() error { + // Always close both engines. + if err := ue.clique.Close(); err != nil { + return err + } + return ue.system.Close() +} + +// SealHash returns the hash of a block prior to it being sealed. +func (ue *UpgradableEngine) SealHash(header *types.Header) common.Hash { + return ue.chooseEngine(header).SealHash(header) +} + +// Authorize injects a private key into the consensus engine to mint new blocks +// with. +func (ue *UpgradableEngine) Authorize(signer common.Address, signFn clique.SignerFn, signFn2 system_contract.SignerFn) { + if cliqueEngine, ok := ue.clique.(*clique.Clique); ok { + cliqueEngine.Authorize(signer, signFn) + } + if sysContractEngine, ok := ue.system.(*system_contract.SystemContract); ok { + sysContractEngine.Authorize(signer, signFn2) + } +} diff --git a/core/types/block.go b/core/types/block.go index bee3dee5168c..644291c52992 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -86,6 +86,12 @@ type Header struct { // BaseFee was added by EIP-1559 and is ignored in legacy headers. BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` + // BlockSignature was added by EuclidV2 to make Extra empty and is ignored during hashing + BlockSignature []byte `json:"-" rlp:"optional"` + + // IsEuclidV2 was added by EuclidV2 to make Extra empty and is ignored during hashing + IsEuclidV2 bool `json:"-" rlp:"optional"` + // WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers. // Included for Ethereum compatibility in Scroll SDK WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` @@ -101,6 +107,9 @@ type Header struct { // ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers. // Included for Ethereum compatibility in Scroll SDK ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + + //Hacky: used internally to mark the header as requested by the downloader at the deliver queue + Requested bool `json:"-" rlp:"-"` } // field type overrides for gencodec @@ -118,7 +127,13 @@ type headerMarshaling struct { // Hash returns the block hash of the header, which is simply the keccak256 hash of its // RLP encoding. func (h *Header) Hash() common.Hash { - return rlpHash(h) + hCopy := CopyHeader(h) + hCopy.BlockSignature = nil + if hCopy.IsEuclidV2 { + hCopy.IsEuclidV2 = false + hCopy.Extra = nil + } + return rlpHash(hCopy) } var headerSize = common.StorageSize(reflect.TypeOf(Header{}).Size()) @@ -153,6 +168,14 @@ func (h *Header) SanityCheck() error { return nil } +// NetworkCompatibleEuclidV2Fields Enforces that both IsEuclidV2 and BlockSignature are empty when received over the network +func (h *Header) NetworkCompatibleEuclidV2Fields() error { + if h.IsEuclidV2 || h.BlockSignature != nil { + return fmt.Errorf("header contains disallowed Euclid V2 fields (only used locally)") + } + return nil +} + // EmptyBody returns true if there is no additional 'body' to complete the header // that is: no transactions and no uncles. func (h *Header) EmptyBody() bool { @@ -164,6 +187,22 @@ func (h *Header) EmptyReceipts() bool { return h.ReceiptHash == EmptyRootHash } +func (h *Header) PrepareForNetwork() { + if h.IsEuclidV2 { + h.IsEuclidV2 = false + h.Extra = h.BlockSignature + h.BlockSignature = nil + } +} + +func (h *Header) PrepareFromNetwork(isEuclidV2 bool) { + if isEuclidV2 { + h.IsEuclidV2 = true + h.BlockSignature = h.Extra + h.Extra = nil + } +} + // Body is a simple (mutable, non-safe) data container for storing and moving // a block's data contents (transactions and uncles) together. type Body struct { @@ -262,6 +301,10 @@ func CopyHeader(h *Header) *Header { cpy.Extra = make([]byte, len(h.Extra)) copy(cpy.Extra, h.Extra) } + if len(h.BlockSignature) > 0 { + cpy.BlockSignature = make([]byte, len(h.BlockSignature)) + copy(cpy.BlockSignature, h.BlockSignature) + } return &cpy } @@ -460,6 +503,22 @@ func (b *Block) CountL2Tx() int { return count } +func (b *Block) CopyBlockDeepWithHeader(header *Header) *Block { + return &Block{ + header: CopyHeader(header), + uncles: b.uncles, // slice reference (the slice header is copied but the underlying array is shared) + transactions: b.transactions, // reference copy + // caches (atomic.Value fields) are reused as-is; if necessary, you might want to load and store their values + hash: b.hash, + size: b.size, + l1MsgCount: b.l1MsgCount, + // Other fields are copied by value (td is a pointer so it's shared) + td: b.td, + ReceivedAt: b.ReceivedAt, + ReceivedFrom: b.ReceivedFrom, + } +} + type Blocks []*Block type BlockWithRowConsumption struct { diff --git a/eth/backend.go b/eth/backend.go index 9dd822be21ec..d6902f089d80 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -32,6 +32,8 @@ import ( "github.com/scroll-tech/go-ethereum/common/hexutil" "github.com/scroll-tech/go-ethereum/consensus" "github.com/scroll-tech/go-ethereum/consensus/clique" + "github.com/scroll-tech/go-ethereum/consensus/system_contract" + "github.com/scroll-tech/go-ethereum/consensus/wrapper" "github.com/scroll-tech/go-ethereum/core" "github.com/scroll-tech/go-ethereum/core/bloombits" "github.com/scroll-tech/go-ethereum/core/rawdb" @@ -156,7 +158,7 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client l1.Client) (*Ether chainDb: chainDb, eventMux: stack.EventMux(), accountManager: stack.AccountManager(), - engine: ethconfig.CreateConsensusEngine(stack, chainConfig, ðashConfig, config.Miner.Notify, config.Miner.Noverify, chainDb), + engine: ethconfig.CreateConsensusEngine(stack, chainConfig, ðashConfig, config.Miner.Notify, config.Miner.Noverify, chainDb, l1Client), closeBloomHandler: make(chan struct{}), networkID: config.NetworkId, gasPrice: config.Miner.GasPrice, @@ -279,6 +281,7 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client l1.Client) (*Ether config.Miner.SigningDisabled = config.DA.ProduceBlocks eth.miner = miner.New(eth, &config.Miner, eth.blockchain.Config(), eth.EventMux(), eth.engine, eth.isLocalBlock, config.EnableDASyncing && !config.DA.ProduceBlocks) + // Some of the extraData is used with Clique consensus (before EuclidV2). After EuclidV2 we use SystemContract consensus where this is overridden when creating a block. eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil} @@ -528,13 +531,27 @@ func (s *Ethereum) StartMining(threads int) error { log.Error("Cannot start mining without etherbase", "err", err) return fmt.Errorf("etherbase missing: %v", err) } - if clique, ok := s.engine.(*clique.Clique); ok { + if wrapper, ok := s.engine.(*wrapper.UpgradableEngine); ok { + wallet, err := s.accountManager.Find(accounts.Account{Address: eb}) + if wallet == nil || err != nil { + log.Error("Etherbase account unavailable locally", "err", err) + return fmt.Errorf("signer missing: %v", err) + } + wrapper.Authorize(eb, wallet.SignData, wallet.SignData) + } else if clique, ok := s.engine.(*clique.Clique); ok { wallet, err := s.accountManager.Find(accounts.Account{Address: eb}) if wallet == nil || err != nil { log.Error("Etherbase account unavailable locally", "err", err) return fmt.Errorf("signer missing: %v", err) } clique.Authorize(eb, wallet.SignData) + } else if systemContract, ok := s.engine.(*system_contract.SystemContract); ok { + wallet, err := s.accountManager.Find(accounts.Account{Address: eb}) + if wallet == nil || err != nil { + log.Error("Etherbase account unavailable locally", "err", err) + return fmt.Errorf("signer missing: %v", err) + } + systemContract.Authorize(eb, wallet.SignData) } // If mining is started, we can disable the transaction rejection mechanism // introduced to speed sync times. diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 839ebe733885..6afb3a852744 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -702,9 +702,11 @@ func (d *Downloader) fetchHead(p *peerConnection) (head *types.Header, pivot *ty // calculateRequestSpan calculates what headers to request from a peer when trying to determine the // common ancestor. // It returns parameters to be used for peer.RequestHeadersByNumber: -// from - starting block number -// count - number of headers to request -// skip - number of headers to skip +// +// from - starting block number +// count - number of headers to request +// skip - number of headers to skip +// // and also returns 'max', the last block which is expected to be returned by the remote peers, // given the (from,count,skip) func calculateRequestSpan(remoteHeight, localHeight uint64) (int64, int, int, uint64) { @@ -1319,22 +1321,22 @@ func (d *Downloader) fetchReceipts(from uint64) error { // various callbacks to handle the slight differences between processing them. // // The instrumentation parameters: -// - errCancel: error type to return if the fetch operation is cancelled (mostly makes logging nicer) -// - deliveryCh: channel from which to retrieve downloaded data packets (merged from all concurrent peers) -// - deliver: processing callback to deliver data packets into type specific download queues (usually within `queue`) -// - wakeCh: notification channel for waking the fetcher when new tasks are available (or sync completed) -// - expire: task callback method to abort requests that took too long and return the faulty peers (traffic shaping) -// - pending: task callback for the number of requests still needing download (detect completion/non-completability) -// - inFlight: task callback for the number of in-progress requests (wait for all active downloads to finish) -// - throttle: task callback to check if the processing queue is full and activate throttling (bound memory use) -// - reserve: task callback to reserve new download tasks to a particular peer (also signals partial completions) -// - fetchHook: tester callback to notify of new tasks being initiated (allows testing the scheduling logic) -// - fetch: network callback to actually send a particular download request to a physical remote peer -// - cancel: task callback to abort an in-flight download request and allow rescheduling it (in case of lost peer) -// - capacity: network callback to retrieve the estimated type-specific bandwidth capacity of a peer (traffic shaping) -// - idle: network callback to retrieve the currently (type specific) idle peers that can be assigned tasks -// - setIdle: network callback to set a peer back to idle and update its estimated capacity (traffic shaping) -// - kind: textual label of the type being downloaded to display in log messages +// - errCancel: error type to return if the fetch operation is cancelled (mostly makes logging nicer) +// - deliveryCh: channel from which to retrieve downloaded data packets (merged from all concurrent peers) +// - deliver: processing callback to deliver data packets into type specific download queues (usually within `queue`) +// - wakeCh: notification channel for waking the fetcher when new tasks are available (or sync completed) +// - expire: task callback method to abort requests that took too long and return the faulty peers (traffic shaping) +// - pending: task callback for the number of requests still needing download (detect completion/non-completability) +// - inFlight: task callback for the number of in-progress requests (wait for all active downloads to finish) +// - throttle: task callback to check if the processing queue is full and activate throttling (bound memory use) +// - reserve: task callback to reserve new download tasks to a particular peer (also signals partial completions) +// - fetchHook: tester callback to notify of new tasks being initiated (allows testing the scheduling logic) +// - fetch: network callback to actually send a particular download request to a physical remote peer +// - cancel: task callback to abort an in-flight download request and allow rescheduling it (in case of lost peer) +// - capacity: network callback to retrieve the estimated type-specific bandwidth capacity of a peer (traffic shaping) +// - idle: network callback to retrieve the currently (type specific) idle peers that can be assigned tasks +// - setIdle: network callback to set a peer back to idle and update its estimated capacity (traffic shaping) +// - kind: textual label of the type being downloaded to display in log messages func (d *Downloader) fetchParts(deliveryCh chan dataPack, deliver func(dataPack) (int, error), wakeCh chan bool, expire func() map[string]int, pending func() int, inFlight func() bool, reserve func(*peerConnection, int) (*fetchRequest, bool, bool), fetchHook func([]*types.Header), fetch func(*peerConnection, *fetchRequest) error, cancel func(*fetchRequest), capacity func(*peerConnection) int, diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index 158608ccd7f9..d27d1b5b8e80 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -477,9 +477,10 @@ func (q *queue) ReserveReceipts(p *peerConnection, count int) (*fetchRequest, bo // to access the queue, so they already need a lock anyway. // // Returns: -// item - the fetchRequest -// progress - whether any progress was made -// throttle - if the caller should throttle for a while +// +// item - the fetchRequest +// progress - whether any progress was made +// throttle - if the caller should throttle for a while func (q *queue) reserveHeaders(p *peerConnection, count int, taskPool map[common.Hash]*types.Header, taskQueue *prque.Prque, pendPool map[string]*fetchRequest, kind uint) (*fetchRequest, bool, bool) { // Short circuit if the pool has been depleted, or if the peer's already @@ -705,6 +706,11 @@ func (q *queue) DeliverHeaders(id string, headers []*types.Header, headerProcCh headerReqTimer.UpdateSince(request.Time) delete(q.headerPendPool, id) + // Hacky: mark that the header was explicitly requested + for _, header := range headers { + header.Requested = true + } + // Ensure headers can be mapped onto the skeleton chain target := q.headerTaskPool[request.From].Hash() @@ -842,6 +848,11 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, reqTimer.UpdateSince(request.Time) delete(pendPool, id) + // Hacky: mark that the header was explicitly requested + for _, header := range request.Headers { + header.Requested = true + } + // If no data items were retrieved, mark them as unavailable for the origin peer if results == 0 { for _, header := range request.Headers { diff --git a/eth/downloader/resultstore.go b/eth/downloader/resultstore.go index 8075f507db3a..4a40e0ebd39e 100644 --- a/eth/downloader/resultstore.go +++ b/eth/downloader/resultstore.go @@ -71,10 +71,11 @@ func (r *resultStore) SetThrottleThreshold(threshold uint64) uint64 { // wants to reserve headers for fetching. // // It returns the following: -// stale - if true, this item is already passed, and should not be requested again -// throttled - if true, the store is at capacity, this particular header is not prio now -// item - the result to store data into -// err - any error that occurred +// +// stale - if true, this item is already passed, and should not be requested again +// throttled - if true, the store is at capacity, this particular header is not prio now +// item - the result to store data into +// err - any error that occurred func (r *resultStore) AddFetch(header *types.Header, fastSync bool) (stale, throttled bool, item *fetchResult, err error) { r.lock.Lock() defer r.lock.Unlock() diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index e8c7a5aa178c..94fa46c34c31 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -18,6 +18,7 @@ package ethconfig import ( + "context" "math/big" "os" "os/user" @@ -25,10 +26,13 @@ import ( "runtime" "time" + "github.com/scroll-tech/go-ethereum/consensus/wrapper" + "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/consensus" "github.com/scroll-tech/go-ethereum/consensus/clique" "github.com/scroll-tech/go-ethereum/consensus/ethash" + "github.com/scroll-tech/go-ethereum/consensus/system_contract" "github.com/scroll-tech/go-ethereum/core" "github.com/scroll-tech/go-ethereum/eth/downloader" "github.com/scroll-tech/go-ethereum/eth/gasprice" @@ -38,6 +42,7 @@ import ( "github.com/scroll-tech/go-ethereum/node" "github.com/scroll-tech/go-ethereum/params" "github.com/scroll-tech/go-ethereum/rollup/da_syncer" + "github.com/scroll-tech/go-ethereum/rollup/sync_service" ) // FullNodeGPO contains default gasprice oracle settings for full node. @@ -228,11 +233,27 @@ type Config struct { } // CreateConsensusEngine creates a consensus engine for the given chain configuration. -func CreateConsensusEngine(stack *node.Node, chainConfig *params.ChainConfig, config *ethash.Config, notify []string, noverify bool, db ethdb.Database) consensus.Engine { - // If proof-of-authority is requested, set it up +func CreateConsensusEngine(stack *node.Node, chainConfig *params.ChainConfig, config *ethash.Config, notify []string, noverify bool, db ethdb.Database, l1Client sync_service.EthClient) consensus.Engine { + // Case 1: Both SystemContract and Clique are defined: create an upgradable engine. + if chainConfig.SystemContract != nil && chainConfig.Clique != nil { + cliqueEngine := clique.New(chainConfig.Clique, db) + sysEngine := system_contract.New(context.Background(), chainConfig.SystemContract, l1Client) + sysEngine.Start() + return wrapper.NewUpgradableEngine(chainConfig.IsEuclidV2, cliqueEngine, sysEngine) + } + + // Case 2: Only the Clique engine is defined. if chainConfig.Clique != nil { return clique.New(chainConfig.Clique, db) } + + // Case 3: Only the SystemContract engine is defined. + if chainConfig.SystemContract != nil { + sysEngine := system_contract.New(context.Background(), chainConfig.SystemContract, l1Client) + sysEngine.Start() + return sysEngine + } + // Otherwise assume proof-of-work switch config.PowMode { case ethash.ModeFake: diff --git a/eth/handler_eth_test.go b/eth/handler_eth_test.go index d24705cec0df..f5e18d2ad1c1 100644 --- a/eth/handler_eth_test.go +++ b/eth/handler_eth_test.go @@ -24,6 +24,8 @@ import ( "testing" "time" + "github.com/scroll-tech/go-ethereum/crypto" + "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/consensus/ethash" "github.com/scroll-tech/go-ethereum/core" @@ -48,7 +50,26 @@ type testEthHandler struct { txBroadcasts event.Feed } -func (h *testEthHandler) Chain() *core.BlockChain { panic("no backing chain") } +func (h *testEthHandler) Chain() *core.BlockChain { + chainConfig := ¶ms.ChainConfig{} + engine := ethash.NewFaker() + chaindb := rawdb.NewMemoryDatabase() + + // Import the canonical chain + cacheConfig := &core.CacheConfig{} + key1, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr1 := crypto.PubkeyToAddress(key1.PublicKey) + genesis := core.GenesisBlockForTesting(chaindb, addr1, big.NewInt(1000000)) + rawdb.WriteBlock(chaindb, genesis) + rawdb.WriteCanonicalHash(chaindb, genesis.Hash(), genesis.NumberU64()) + rawdb.WriteHeadBlockHash(chaindb, genesis.Hash()) + + chain, err := core.NewBlockChain(chaindb, cacheConfig, chainConfig, engine, vm.Config{}, nil, nil) + if err != nil { + panic(fmt.Sprintf("failed to create chain: %v", err)) + } + return chain +} func (h *testEthHandler) StateBloom() *trie.SyncBloom { panic("no backing state bloom") } func (h *testEthHandler) TxPool() eth.TxPool { panic("no backing tx pool") } func (h *testEthHandler) AcceptTxs() bool { return true } @@ -505,8 +526,7 @@ func testCheckpointChallenge(t *testing.T, syncmode downloader.SyncMode, checkpo var response *types.Header if checkpoint { number := (uint64(rand.Intn(500))+1)*params.CHTFrequency - 1 - response = &types.Header{Number: big.NewInt(int64(number)), Extra: []byte("valid")} - + response = &types.Header{Number: big.NewInt(int64(number))} handler.handler.checkpointNumber = number handler.handler.checkpointHash = response.Hash() } @@ -554,6 +574,7 @@ func testCheckpointChallenge(t *testing.T, syncmode downloader.SyncMode, checkpo query.Origin.Number, query.Amount, query.Skip, query.Reverse, response.Number.Uint64(), 1, 0, false) } + // Create a block to reply to the challenge if no timeout is simulated. if !timeout { if empty { @@ -565,7 +586,8 @@ func testCheckpointChallenge(t *testing.T, syncmode downloader.SyncMode, checkpo t.Fatalf("failed to answer challenge: %v", err) } } else { - if err := remote.ReplyBlockHeaders(request.RequestId, []*types.Header{{Number: response.Number}}); err != nil { + number := new(big.Int).Add(response.Number, big.NewInt(1)) // mismatching headers to same request + if err := remote.ReplyBlockHeaders(request.RequestId, []*types.Header{{Number: number}}); err != nil { t.Fatalf("failed to answer challenge: %v", err) } } @@ -573,7 +595,6 @@ func testCheckpointChallenge(t *testing.T, syncmode downloader.SyncMode, checkpo } // Wait until the test timeout passes to ensure proper cleanup time.Sleep(syncChallengeTimeout + 300*time.Millisecond) - // Verify that the remote peer is maintained or dropped. if drop { <-handlerDone diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index 367d9a4dbc75..766bc1370fff 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -24,6 +24,7 @@ import ( "github.com/scroll-tech/go-ethereum/core/types" "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/metrics" + "github.com/scroll-tech/go-ethereum/p2p" "github.com/scroll-tech/go-ethereum/rlp" "github.com/scroll-tech/go-ethereum/trie" ) @@ -266,6 +267,16 @@ func handleNewBlock(backend Backend, msg Decoder, peer *Peer) error { if err := msg.Decode(ann); err != nil { return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) } + + hCopy := ann.Block.Header() + if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil { + peer.Peer.Disconnect(p2p.DiscUselessPeer) + return err + } + + hCopy.PrepareFromNetwork(backend.Chain().Config().IsEuclidV2(hCopy.Time)) + ann.Block = ann.Block.CopyBlockDeepWithHeader(hCopy) + if err := ann.sanityCheck(); err != nil { return err } @@ -294,6 +305,17 @@ func handleBlockHeaders66(backend Backend, msg Decoder, peer *Peer) error { } requestTracker.Fulfil(peer.id, peer.version, BlockHeadersMsg, res.RequestId) + headersCopy := make([]*types.Header, 0, len(res.BlockHeadersPacket)) + for _, header := range res.BlockHeadersPacket { + hCopy := types.CopyHeader(header) + if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil { + peer.Peer.Disconnect(p2p.DiscUselessPeer) + return err + } + hCopy.PrepareFromNetwork(backend.Chain().Config().IsEuclidV2(header.Time)) + headersCopy = append(headersCopy, hCopy) + } + res.BlockHeadersPacket = headersCopy return backend.Handle(peer, &res.BlockHeadersPacket) } diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index 8a012868ac8f..d7ab78922723 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -277,6 +277,9 @@ func (p *Peer) AsyncSendNewBlockHash(block *types.Block) { func (p *Peer) SendNewBlock(block *types.Block, td *big.Int) error { // Mark all the block hash as known, but ensure we don't overflow our limits p.knownBlocks.Add(block.Hash()) + hCopy := block.Header() + hCopy.PrepareForNetwork() + block = block.CopyBlockDeepWithHeader(hCopy) return p2p.Send(p.rw, NewBlockMsg, &NewBlockPacket{ Block: block, TD: td, @@ -297,6 +300,14 @@ func (p *Peer) AsyncSendNewBlock(block *types.Block, td *big.Int) { // ReplyBlockHeaders is the eth/66 version of SendBlockHeaders. func (p *Peer) ReplyBlockHeaders(id uint64, headers []*types.Header) error { + headersCopy := make([]*types.Header, 0, len(headers)) + for _, header := range headers { + hCopy := types.CopyHeader(header) + hCopy.PrepareForNetwork() + headersCopy = append(headersCopy, hCopy) + } + headers = headersCopy + return p2p.Send(p.rw, BlockHeadersMsg, BlockHeadersPacket66{ RequestId: id, BlockHeadersPacket: headers, diff --git a/eth/protocols/snap/sync_test.go b/eth/protocols/snap/sync_test.go index db898241ee98..2aa668542d6f 100644 --- a/eth/protocols/snap/sync_test.go +++ b/eth/protocols/snap/sync_test.go @@ -369,7 +369,8 @@ func createStorageRequestResponse(t *testPeer, root common.Hash, accounts []comm return hashes, slots, proofs } -// the createStorageRequestResponseAlwaysProve tests a cornercase, where it always +// the createStorageRequestResponseAlwaysProve tests a cornercase, where it always +// // supplies the proof for the last account, even if it is 'complete'.h func createStorageRequestResponseAlwaysProve(t *testPeer, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max uint64) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) { var size uint64 diff --git a/eth/tracers/js/internal/tracers/assets.go b/eth/tracers/js/internal/tracers/assets.go index caeccb7f3655..15e7757a7d39 100644 --- a/eth/tracers/js/internal/tracers/assets.go +++ b/eth/tracers/js/internal/tracers/assets.go @@ -388,11 +388,13 @@ const AssetDebug = false // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png +// +// data/ +// foo.txt +// img/ +// a.png +// b.png +// // then AssetDir("data") would return []string{"foo.txt", "img"}, // AssetDir("data/img") would return []string{"a.png", "b.png"}, // AssetDir("foo.txt") and AssetDir("notexist") would return an error, and diff --git a/eth/tracers/native/4byte.go b/eth/tracers/native/4byte.go index a62da6313766..bad90f828e28 100644 --- a/eth/tracers/native/4byte.go +++ b/eth/tracers/native/4byte.go @@ -38,14 +38,15 @@ func init() { // a reversed signature can be matched against the size of the data. // // Example: -// > debug.traceTransaction( "0x214e597e35da083692f5386141e69f47e973b2c56e7a8073b1ea08fd7571e9de", {tracer: "4byteTracer"}) -// { -// 0x27dc297e-128: 1, -// 0x38cc4831-0: 2, -// 0x524f3889-96: 1, -// 0xadf59f99-288: 1, -// 0xc281d19e-0: 1 -// } +// +// > debug.traceTransaction( "0x214e597e35da083692f5386141e69f47e973b2c56e7a8073b1ea08fd7571e9de", {tracer: "4byteTracer"}) +// { +// 0x27dc297e-128: 1, +// 0x38cc4831-0: 2, +// 0x524f3889-96: 1, +// 0xadf59f99-288: 1, +// 0xc281d19e-0: 1 +// } type fourByteTracer struct { env *vm.EVM ids map[string]int // ids aggregates the 4byte ids found diff --git a/go.mod b/go.mod index f739a26565b9..e36cb8b5f47d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,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 9c60daf0a630..6e8606ed54b2 100644 --- a/go.sum +++ b/go.sum @@ -134,8 +134,8 @@ github.com/ethereum/c-kzg-4844/bindings/go v0.0.0-20230126171313-363c7d7593b4 h1 github.com/ethereum/c-kzg-4844/bindings/go v0.0.0-20230126171313-363c7d7593b4/go.mod h1:y4GA2JbAUama1S4QwYjC2hefgGLU8Ul0GMtL/ADMF1c= 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/les/api_test.go b/les/api_test.go index ad47ff02d6a1..28634c7c86d0 100644 --- a/les/api_test.go +++ b/les/api_test.go @@ -498,7 +498,7 @@ func newLesClientService(ctx *adapters.ServiceContext, stack *node.Node) (node.L config := ethconfig.Defaults config.SyncMode = (ethdownloader.SyncMode)(downloader.LightSync) config.Ethash.PowMode = ethash.ModeFake - return New(stack, &config) + return New(stack, &config, nil) } func newLesServerService(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { diff --git a/les/client.go b/les/client.go index 311f78a658a5..2918d4d68f9d 100644 --- a/les/client.go +++ b/les/client.go @@ -46,6 +46,7 @@ import ( "github.com/scroll-tech/go-ethereum/p2p/enr" "github.com/scroll-tech/go-ethereum/params" "github.com/scroll-tech/go-ethereum/rlp" + "github.com/scroll-tech/go-ethereum/rollup/sync_service" "github.com/scroll-tech/go-ethereum/rpc" ) @@ -79,7 +80,7 @@ type LightEthereum struct { } // New creates an instance of the light client. -func New(stack *node.Node, config *ethconfig.Config) (*LightEthereum, error) { +func New(stack *node.Node, config *ethconfig.Config, l1Client sync_service.EthClient) (*LightEthereum, error) { chainDb, err := stack.OpenDatabase("lightchaindata", config.DatabaseCache, config.DatabaseHandles, "eth/db/chaindata/", false) if err != nil { return nil, err @@ -109,7 +110,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*LightEthereum, error) { eventMux: stack.EventMux(), reqDist: newRequestDistributor(peers, &mclock.System{}), accountManager: stack.AccountManager(), - engine: ethconfig.CreateConsensusEngine(stack, chainConfig, &config.Ethash, nil, false, chainDb), + engine: ethconfig.CreateConsensusEngine(stack, chainConfig, &config.Ethash, nil, false, chainDb, l1Client), bloomRequests: make(chan chan *bloombits.Retrieval), bloomIndexer: core.NewBloomIndexer(chainDb, params.BloomBitsBlocksClient, params.HelperTrieConfirmations), p2pServer: stack.Server(), diff --git a/les/downloader/queue.go b/les/downloader/queue.go index 158608ccd7f9..dbb879d940c2 100644 --- a/les/downloader/queue.go +++ b/les/downloader/queue.go @@ -705,6 +705,11 @@ func (q *queue) DeliverHeaders(id string, headers []*types.Header, headerProcCh headerReqTimer.UpdateSince(request.Time) delete(q.headerPendPool, id) + // Hacky: mark that the header was explicitly requested + for _, header := range headers { + header.Requested = true + } + // Ensure headers can be mapped onto the skeleton chain target := q.headerTaskPool[request.From].Hash() @@ -842,6 +847,11 @@ func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, reqTimer.UpdateSince(request.Time) delete(pendPool, id) + // Hacky: mark that the header was explicitly requested + for _, header := range request.Headers { + header.Requested = true + } + // If no data items were retrieved, mark them as unavailable for the origin peer if results == 0 { for _, header := range request.Headers { diff --git a/miner/scroll_worker.go b/miner/scroll_worker.go index b58893031996..4ae5413f60e2 100644 --- a/miner/scroll_worker.go +++ b/miner/scroll_worker.go @@ -25,6 +25,8 @@ import ( "sync/atomic" "time" + "github.com/scroll-tech/go-ethereum/consensus/system_contract" + "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/consensus" "github.com/scroll-tech/go-ethereum/consensus/misc" @@ -358,8 +360,11 @@ func (w *worker) mainLoop() { } var retryableCommitError *retryableCommitError - if errors.As(err, &retryableCommitError) { + if errors.As(err, &retryableCommitError) || errors.Is(err, system_contract.ErrUnauthorizedSigner) { log.Warn("failed to commit to a block, retrying", "err", err) + if errors.Is(err, system_contract.ErrUnauthorizedSigner) { + time.Sleep(5 * time.Second) // half the time it takes for the system contract consensus to read and update the address locally. + } if _, err = w.tryCommitNewWork(time.Now(), w.current.header.ParentHash, w.current.reorging, w.current.reorgReason); err != nil { continue } diff --git a/mobile/geth.go b/mobile/geth.go index 6b1dfebb1bf8..d1c252adfc4b 100644 --- a/mobile/geth.go +++ b/mobile/geth.go @@ -194,7 +194,7 @@ func NewNode(datadir string, config *NodeConfig) (stack *Node, _ error) { ethConf.SyncMode = downloader.LightSync ethConf.NetworkId = uint64(config.EthereumNetworkID) ethConf.DatabaseCache = config.EthereumDatabaseCache - lesBackend, err := les.New(rawStack, ðConf) + lesBackend, err := les.New(rawStack, ðConf, nil) if err != nil { return nil, fmt.Errorf("ethereum init: %v", err) } diff --git a/params/config.go b/params/config.go index d47286107d51..af6d82377273 100644 --- a/params/config.go +++ b/params/config.go @@ -648,8 +648,9 @@ type ChainConfig struct { TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` // Various consensus engines - Ethash *EthashConfig `json:"ethash,omitempty"` - Clique *CliqueConfig `json:"clique,omitempty"` + Ethash *EthashConfig `json:"ethash,omitempty"` + Clique *CliqueConfig `json:"clique,omitempty"` + SystemContract *SystemContractConfig `json:"systemContract,omitempty"` // Scroll genesis extension: enable scroll rollup-related traces & state transition Scroll ScrollConfig `json:"scroll,omitempty"` @@ -771,6 +772,21 @@ func (c *CliqueConfig) String() string { return "clique" } +// SystemContractConfig is the consensus engine configs for rollup sequencer sealing. +type SystemContractConfig struct { + Period uint64 `json:"period"` // Number of seconds between blocks to enforce + + SystemContractAddress common.Address `json:"system_contract_address"` // address of system contract on L1 + SystemContractSlot common.Hash `json:"system_contract_slot"` // slot of signer address in system contract on L1 + + RelaxedPeriod bool `json:"relaxed_period"` // Relaxes the period to be just an upper bound +} + +// String implements the stringer interface, returning the consensus engine details. +func (c *SystemContractConfig) String() string { + return "system_contract" +} + // String implements the fmt.Stringer interface. func (c *ChainConfig) String() string { var engine interface{} diff --git a/params/version.go b/params/version.go index f904740d6565..b5ef4640b6e3 100644 --- a/params/version.go +++ b/params/version.go @@ -24,7 +24,7 @@ import ( const ( VersionMajor = 5 // Major version component of the current release VersionMinor = 8 // Minor version component of the current release - VersionPatch = 13 // Patch version component of the current release + VersionPatch = 14 // Patch version component of the current release VersionMeta = "mainnet" // Version metadata to append to the version string ) diff --git a/rollup/l1/types.go b/rollup/l1/types.go index 0adb734bac09..209d3956da97 100644 --- a/rollup/l1/types.go +++ b/rollup/l1/types.go @@ -19,6 +19,7 @@ type Client interface { TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, isPending bool, err error) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + StorageAt(ctx context.Context, contract common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) } type MockNopClient struct{} @@ -58,3 +59,7 @@ func (m *MockNopClient) BlockByHash(ctx context.Context, hash common.Hash) (*typ func (m *MockNopClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { return nil, nil } + +func (m *MockNopClient) StorageAt(ctx context.Context, contract common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { + return nil, nil +} diff --git a/rollup/sync_service/types.go b/rollup/sync_service/types.go index 3429ec1bb778..08b4dda2ecf0 100644 --- a/rollup/sync_service/types.go +++ b/rollup/sync_service/types.go @@ -19,4 +19,5 @@ type EthClient interface { SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, isPending bool, err error) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) }