feat: add system config consensus to deprecate clique (#1102)

* feat: add system config consensus to deprecate poa

* fix: check extra field lens

* Finish implementation with one signer only

* Implement unit tests

* Only check signature if block not requested

* Remove Extra from rlp encoding (and hashing)

* Implement UpgradableEngine as middleware for Clique/SystemContract

* Use isEuclid, add json tag to Requested, make go idiomatic

* Changes post integration test

* Remove comment

* Address comments

* Merge

* New field BlockSignature (not used in hashing/JSON)

* Enforce .Extra to be an empty slice

* replace header.Requested for header.IsNewBlock

* mark new block as IsNewBlock

* Make new RLP optional fields always default for legacy/reth compatibility

* Placing new fields as last non-zero rlp:optional values used by Scroll

* Penalize nodes that send non-zero Euclid V2 header field values

* Bring back .Requested to downloader instead of .IsNewBlock

* Replace IsNewBlock for Requested

* Address comments

* merge

* prevent timing issues in tests

* fix ci

* chore: auto version bump [bot]

* Update consensus/system_contract/consensus.go

Co-authored-by: Morty <70688412+yiweichi@users.noreply.github.com>

* chore: auto version bump [bot]

* Update consensus/system_contract/consensus.go

Co-authored-by: Jonas Theis <4181434+jonastheis@users.noreply.github.com>

* Remove whitespaces and merge version number

* chore: auto version bump [bot]

* validate that the read address from L1 is not empty and improved error handling when fetching address

* Fix indentation issue

* Fix test

* goimports fix

* goimports

---------

Co-authored-by: Alejandro Ranchal-Pedrosa <a.ranchalpedrosa@gmail.com>
Co-authored-by: jonastheis <4181434+jonastheis@users.noreply.github.com>
Co-authored-by: jonastheis <jonastheis@users.noreply.github.com>
Co-authored-by: ranchalp <ranchalp@users.noreply.github.com>
Co-authored-by: Péter Garamvölgyi <peter@scroll.io>
This commit is contained in:
Morty 2025-02-28 22:53:32 +08:00 committed by GitHub
parent bcfdb48dd9
commit 365b82953d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1240 additions and 72 deletions

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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 <nil>", 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())
}
}

View file

@ -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
}

View file

@ -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 := &params.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 := &params.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 := &params.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
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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, &ethashConfig, config.Miner.Notify, config.Miner.Noverify, chainDb),
engine: ethconfig.CreateConsensusEngine(stack, chainConfig, &ethashConfig, 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.

View file

@ -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,

View file

@ -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 {

View file

@ -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()

View file

@ -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:

View file

@ -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 := &params.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

View file

@ -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)
}

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

2
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

View file

@ -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) {

View file

@ -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(),

View file

@ -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 {

View file

@ -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
}

View file

@ -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, &ethConf)
lesBackend, err := les.New(rawStack, &ethConf, nil)
if err != nil {
return nil, fmt.Errorf("ethereum init: %v", err)
}

View file

@ -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{}

View file

@ -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
)

View file

@ -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
}

View file

@ -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)
}