feat: address system contract consensus issues and comments (#1126)

* feat: address system contract consensus issues and comments

* fix Euclid header chain verification
This commit is contained in:
Péter Garamvölgyi 2025-03-02 21:01:28 +01:00 committed by GitHub
parent 365b82953d
commit 1dedddbffb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 128 additions and 70 deletions

View file

@ -191,6 +191,8 @@ type Clique struct {
// New creates a Clique proof-of-authority consensus engine with the initial // New creates a Clique proof-of-authority consensus engine with the initial
// signers set to the ones provided by the user. // signers set to the ones provided by the user.
func New(config *params.CliqueConfig, db ethdb.Database) *Clique { func New(config *params.CliqueConfig, db ethdb.Database) *Clique {
log.Info("Initializing clique consensus engine")
// Set any missing consensus parameters to their defaults // Set any missing consensus parameters to their defaults
conf := *config conf := *config
if conf.Epoch == 0 { if conf.Epoch == 0 {

View file

@ -2,17 +2,20 @@ package system_contract
import ( import (
"github.com/scroll-tech/go-ethereum/common" "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 // API is a user facing RPC API to allow controlling the signer and voting
// mechanisms of the proof-of-authority scheme. // mechanisms of the proof-of-authority scheme.
type API struct { type API struct {
chain consensus.ChainHeaderReader system_contract *SystemContract
} }
// GetSigners retrieves the list of authorized signers at the specified block. // GetSigners retrieves the list of authorized signers at the specified block.
func (api *API) GetSigners(number *rpc.BlockNumber) ([]common.Address, error) { func (api *API) GetLocalSigner() (common.Address, error) {
return nil, nil return api.system_contract.localSignerAddress(), nil
}
// GetSigners retrieves the list of authorized signers at the specified block.
func (api *API) GetAuthorizedSigner() (common.Address, error) {
return api.system_contract.currentSignerAddressL1(), nil
} }

View file

@ -36,20 +36,27 @@ var (
// errUnknownBlock is returned when the list of signers is requested for a block // errUnknownBlock is returned when the list of signers is requested for a block
// that is not part of the local blockchain. // that is not part of the local blockchain.
errUnknownBlock = errors.New("unknown block") errUnknownBlock = errors.New("unknown block")
// errNonceNotEmpty is returned if a nonce value is non-zero // errNonceNotEmpty is returned if a nonce value is non-zero
errInvalidNonce = errors.New("nonce not empty nor zero") errInvalidNonce = errors.New("nonce not empty nor zero")
// errMissingSignature is returned if a block's extra-data section doesn't seem // errMissingSignature is returned if a block's extra-data section doesn't seem
// to contain a 65 byte secp256k1 signature. // to contain a 65 byte secp256k1 signature.
errMissingSignature = errors.New("extra-data 65 byte signature missing") errMissingSignature = errors.New("extra-data 65 byte signature missing")
// errInvalidMixDigest is returned if a block's mix digest is non-zero. // errInvalidMixDigest is returned if a block's mix digest is non-zero.
errInvalidMixDigest = errors.New("non-zero mix digest") errInvalidMixDigest = errors.New("non-zero mix digest")
// errInvalidUncleHash is returned if a block contains an non-empty uncle list. // errInvalidUncleHash is returned if a block contains an non-empty uncle list.
errInvalidUncleHash = errors.New("non empty uncle hash") errInvalidUncleHash = errors.New("non empty uncle hash")
// errInvalidDifficulty is returned if a difficulty value is non-zero // errInvalidDifficulty is returned if a difficulty value is non-zero
errInvalidDifficulty = errors.New("non-one difficulty") errInvalidDifficulty = errors.New("non-one difficulty")
// errInvalidTimestamp is returned if the timestamp of a block is lower than // errInvalidTimestamp is returned if the timestamp of a block is lower than
// the previous block's timestamp + the minimum block period. // the previous block's timestamp.
errInvalidTimestamp = errors.New("invalid timestamp") errInvalidTimestamp = errors.New("invalid timestamp")
// errInvalidExtra is returned if the extra data is not empty // errInvalidExtra is returned if the extra data is not empty
errInvalidExtra = errors.New("invalid extra") errInvalidExtra = errors.New("invalid extra")
) )
@ -335,10 +342,6 @@ func ecrecover(header *types.Header) (common.Address, error) {
// SystemContractRLP returns the rlp bytes which needs to be signed for the system contract // 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 // 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 { func SystemContractRLP(header *types.Header) []byte {
b := new(bytes.Buffer) b := new(bytes.Buffer)
encodeSigHeader(b, header) encodeSigHeader(b, header)
@ -354,8 +357,12 @@ func (s *SystemContract) CalcDifficulty(chain consensus.ChainHeaderReader, time
// controlling the signer voting. // controlling the signer voting.
func (s *SystemContract) APIs(chain consensus.ChainHeaderReader) []rpc.API { func (s *SystemContract) APIs(chain consensus.ChainHeaderReader) []rpc.API {
return []rpc.API{{ return []rpc.API{{
Namespace: "system_contract", // note: cannot use underscore in namespace,
Service: &API{}, // but overlap with another module's name space works fine.
Namespace: "scroll",
Version: "1.0",
Service: &API{system_contract: s},
Public: false,
}} }}
} }

View file

@ -16,10 +16,11 @@ const (
defaultSyncInterval = 10 * time.Second defaultSyncInterval = 10 * time.Second
) )
// SystemContract // SystemContract is the proof-of-authority consensus engine that fetches
// the authorized signer from the SystemConfig contract, starting from EuclidV2.
type SystemContract struct { type SystemContract struct {
config *params.SystemContractConfig // Consensus engine configuration parameters config *params.SystemContractConfig // Consensus engine configuration parameters
client sync_service.EthClient client sync_service.EthClient // RPC client to fetch info from L1
signerAddressL1 common.Address // Address of the signer stored in L1 System Contract signerAddressL1 common.Address // Address of the signer stored in L1 System Contract
@ -32,8 +33,10 @@ type SystemContract struct {
} }
// New creates a SystemContract consensus engine with the initial // New creates a SystemContract consensus engine with the initial
// signers set to the ones provided by the user. // authorized signer fetched from L1 (if available).
func New(ctx context.Context, config *params.SystemContractConfig, client sync_service.EthClient) *SystemContract { func New(ctx context.Context, config *params.SystemContractConfig, client sync_service.EthClient) *SystemContract {
log.Info("Initializing system_contract consensus engine", "config", config)
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
s := &SystemContract{ s := &SystemContract{
@ -53,7 +56,7 @@ func New(ctx context.Context, config *params.SystemContractConfig, client sync_s
// Authorize injects a private key into the consensus engine to mint new blocks // Authorize injects a private key into the consensus engine to mint new blocks
// with. // with.
func (s *SystemContract) Authorize(signer common.Address, signFn SignerFn) { func (s *SystemContract) Authorize(signer common.Address, signFn SignerFn) {
log.Info("Authorizing system contract", "signer", signer.Hex()) log.Info("Authorizing system contract consensus", "signer", signer.Hex())
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -93,19 +96,17 @@ func (s *SystemContract) fetchAddressFromL1() error {
// Validate the address is not empty // Validate the address is not empty
if bAddress == (common.Address{}) { if bAddress == (common.Address{}) {
return fmt.Errorf("retrieved empty signer address from L1 System Contract") return fmt.Errorf("retrieved empty signer address from L1 System Contract: contract=%s, slot=%s", s.config.SystemContractAddress.Hex(), s.config.SystemContractSlot.Hex())
} }
log.Debug("Read address from system contract", "address", bAddress.Hex()) log.Debug("Read address from system contract", "address", bAddress.Hex())
s.lock.RLock() s.lock.Lock()
addressChanged := s.signerAddressL1 != bAddress defer s.lock.Unlock()
s.lock.RUnlock()
if addressChanged { if s.signerAddressL1 != bAddress {
s.lock.Lock()
s.signerAddressL1 = bAddress s.signerAddressL1 = bAddress
s.lock.Unlock() log.Info("Updated new signer from L1 system contract", "signer", bAddress.Hex())
} }
return nil return nil
@ -123,3 +124,10 @@ func (s *SystemContract) currentSignerAddressL1() common.Address {
return s.signerAddressL1 return s.signerAddressL1
} }
func (s *SystemContract) localSignerAddress() common.Address {
s.lock.RLock()
defer s.lock.RUnlock()
return s.signer
}

View file

@ -2,7 +2,7 @@ package wrapper
import ( import (
"math/big" "math/big"
"sync" "time"
"github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/consensus" "github.com/scroll-tech/go-ethereum/consensus"
@ -10,13 +10,14 @@ import (
"github.com/scroll-tech/go-ethereum/consensus/system_contract" "github.com/scroll-tech/go-ethereum/consensus/system_contract"
"github.com/scroll-tech/go-ethereum/core/state" "github.com/scroll-tech/go-ethereum/core/state"
"github.com/scroll-tech/go-ethereum/core/types" "github.com/scroll-tech/go-ethereum/core/types"
"github.com/scroll-tech/go-ethereum/log"
"github.com/scroll-tech/go-ethereum/rpc" "github.com/scroll-tech/go-ethereum/rpc"
) )
// UpgradableEngine implements consensus.Engine and acts as a middleware to dispatch // UpgradableEngine implements consensus.Engine and acts as a middleware to dispatch
// calls to either Clique or SystemContract consensus. // calls to either Clique or SystemContract consensus.
type UpgradableEngine struct { type UpgradableEngine struct {
// forkBlock is the block number at which the switchover to SystemContract occurs. // isUpgraded takes a block timestamp, and returns true once the engine should be upgraded to SystemContract.
isUpgraded func(uint64) bool isUpgraded func(uint64) bool
// clique is the original Clique consensus engine. // clique is the original Clique consensus engine.
@ -28,6 +29,8 @@ type UpgradableEngine struct {
// NewUpgradableEngine constructs a new upgradable consensus middleware. // NewUpgradableEngine constructs a new upgradable consensus middleware.
func NewUpgradableEngine(isUpgraded func(uint64) bool, clique consensus.Engine, system consensus.Engine) *UpgradableEngine { func NewUpgradableEngine(isUpgraded func(uint64) bool, clique consensus.Engine, system consensus.Engine) *UpgradableEngine {
log.Info("Initializing upgradable consensus engine")
return &UpgradableEngine{ return &UpgradableEngine{
isUpgraded: isUpgraded, isUpgraded: isUpgraded,
clique: clique, clique: clique,
@ -35,7 +38,7 @@ func NewUpgradableEngine(isUpgraded func(uint64) bool, clique consensus.Engine,
} }
} }
// chooseEngine returns the appropriate consensus engine based on the header's number. // chooseEngine returns the appropriate consensus engine based on the header's timestamp.
func (ue *UpgradableEngine) chooseEngine(header *types.Header) consensus.Engine { func (ue *UpgradableEngine) chooseEngine(header *types.Header) consensus.Engine {
if ue.isUpgraded(header.Time) { if ue.isUpgraded(header.Time) {
return ue.system return ue.system
@ -60,12 +63,12 @@ func (ue *UpgradableEngine) VerifyHeader(chain consensus.ChainHeaderReader, head
// headers can only be all system, all clique, or start with clique and then switch once to system. // 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) { func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) {
abort := make(chan struct{}) abort := make(chan struct{})
out := make(chan error) results := make(chan error, len(headers))
// If there are no headers, return a closed error channel. // If there are no headers, return a closed error channel.
if len(headers) == 0 { if len(headers) == 0 {
close(out) close(results)
return nil, out return nil, results
} }
// Choose engine for the first and last header. // Choose engine for the first and last header.
@ -97,42 +100,55 @@ func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, hea
systemHeaders := headers[splitIndex:] systemHeaders := headers[splitIndex:]
systemSeals := seals[splitIndex:] systemSeals := seals[splitIndex:]
// Create a wait group to merge results. log.Info("Verifying EuclidV2 transition header chain")
var wg sync.WaitGroup
wg.Add(2)
// Launch concurrent verifications. // Do verification concurrently,
// but make sure to run Clique first, then SystemContract,
// so that the results are sent in the correct order.
go func() { go func() {
defer wg.Done() defer close(results)
_, cliqueResults := ue.clique.VerifyHeaders(chain, cliqueHeaders, cliqueSeals)
for err := range cliqueResults { // Verify clique headers.
log.Info("Start EuclidV2 transition verification in Clique section", "startBlockNumber", cliqueHeaders[0].Number, "endBlockNumber", cliqueHeaders[len(cliqueHeaders)-1].Number)
abortClique, cliqueResults := ue.clique.VerifyHeaders(chain, cliqueHeaders, cliqueSeals)
// Note: cliqueResults is not closed so we cannot directly iterate over it
for i := 0; i < len(cliqueHeaders); i++ {
select { select {
case <-abort: case <-abort:
close(abortClique)
log.Warn("Aborted EuclidV2 transition verification in Clique section")
return return
case out <- err: case err := <-cliqueResults:
results <- err
} }
} }
}()
go func() { // Not sure why we need this here, but without this we get err="unknown ancestor"
defer wg.Done() // at the 1st Euclid block. It seems that `VerifyHeaders` start processing the next
_, systemResults := ue.system.VerifyHeaders(chain, systemHeaders, systemSeals) // header before the previous one was written into `chain`.
for err := range systemResults { time.Sleep(2 * time.Second)
// Verify system contract headers.
log.Info("Start EuclidV2 transition verification in SystemContract section", "startBlockNumber", systemHeaders[0].Number, "endBlockNumber", systemHeaders[len(systemHeaders)-1].Number)
abortSystem, systemResults := ue.system.VerifyHeaders(chain, systemHeaders, systemSeals)
// Note: systemResults is not closed so we cannot directly iterate over it
for i := 0; i < len(systemHeaders); i++ {
select { select {
case <-abort: case <-abort:
close(abortSystem)
log.Info("Aborted EuclidV2 transition verification in SystemContract section")
return return
case out <- err: case err := <-systemResults:
results <- err
} }
} }
log.Info("Completed EuclidV2 transition verification")
}() }()
// Close the out channel when both verifications are complete. return abort, results
go func() {
wg.Wait()
close(out)
}()
return abort, out
} }
// Prepare prepares a block header for sealing. // Prepare prepares a block header for sealing.
@ -167,27 +183,22 @@ func (ue *UpgradableEngine) VerifyUncles(chain consensus.ChainReader, block *typ
// APIs returns any RPC APIs exposed by the consensus engine. // APIs returns any RPC APIs exposed by the consensus engine.
func (ue *UpgradableEngine) APIs(chain consensus.ChainHeaderReader) []rpc.API { func (ue *UpgradableEngine) APIs(chain consensus.ChainHeaderReader) []rpc.API {
// Determine the current chain head. return append(ue.clique.APIs(chain), ue.system.APIs(chain)...)
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. // Close terminates the consensus engine.
func (ue *UpgradableEngine) Close() error { func (ue *UpgradableEngine) Close() error {
// Always close both engines. // Always close both engines.
if err := ue.clique.Close(); err != nil { err1 := ue.clique.Close()
return err err2 := ue.system.Close()
if err1 != nil || err2 != nil {
log.Error("Error while closing upgradable engine", "cliqueError", err1, "systemContractError", err2)
} }
return ue.system.Close() if err1 != nil {
return err1
}
return err2
} }
// SealHash returns the hash of a block prior to it being sealed. // SealHash returns the hash of a block prior to it being sealed.

View file

@ -86,10 +86,17 @@ type Header struct {
// BaseFee was added by EIP-1559 and is ignored in legacy headers. // BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
// BlockSignature was added by EuclidV2 to make Extra empty and is ignored during hashing // Note: The subsequent fields are rlp:"optional". When encoding a struct with optional
// fields, the output RLP list contains all values up to the last non-zero optional field.
// BlockSignature was added by EuclidV2 to make Extra empty and is ignored during hashing.
// This field is stored in db but not included in messages sent on the network wire protocol,
// or in RPC responses. See also `PrepareForNetwork` and `PrepareFromNetwork`.
BlockSignature []byte `json:"-" rlp:"optional"` BlockSignature []byte `json:"-" rlp:"optional"`
// IsEuclidV2 was added by EuclidV2 to make Extra empty and is ignored during hashing // IsEuclidV2 was added by EuclidV2 to make Extra empty and is ignored during hashing.
// This field is stored in db but not included in messages sent on the network wire protocol,
// or in RPC responses. See also `PrepareForNetwork` and `PrepareFromNetwork`.
IsEuclidV2 bool `json:"-" rlp:"optional"` IsEuclidV2 bool `json:"-" rlp:"optional"`
// WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers. // WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.
@ -108,7 +115,9 @@ type Header struct {
// Included for Ethereum compatibility in Scroll SDK // Included for Ethereum compatibility in Scroll SDK
ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
//Hacky: used internally to mark the header as requested by the downloader at the deliver queue // Hacky: used internally to mark the header as requested by the downloader at the deliver queue.
// Note: This is only used internally to mark a previously requested block, it is not included
// in db, on the network wire protocol, or in RPC responses.
Requested bool `json:"-" rlp:"-"` Requested bool `json:"-" rlp:"-"`
} }

View file

@ -270,6 +270,7 @@ func handleNewBlock(backend Backend, msg Decoder, peer *Peer) error {
hCopy := ann.Block.Header() hCopy := ann.Block.Header()
if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil { if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil {
log.Warn("Disconnecting peer in handleNewBlock", "peer", peer.id, "reason", err)
peer.Peer.Disconnect(p2p.DiscUselessPeer) peer.Peer.Disconnect(p2p.DiscUselessPeer)
return err return err
} }
@ -309,6 +310,7 @@ func handleBlockHeaders66(backend Backend, msg Decoder, peer *Peer) error {
for _, header := range res.BlockHeadersPacket { for _, header := range res.BlockHeadersPacket {
hCopy := types.CopyHeader(header) hCopy := types.CopyHeader(header)
if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil { if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil {
log.Warn("Disconnecting peer in handleBlockHeaders66", "peer", peer.id, "reason", err)
peer.Peer.Disconnect(p2p.DiscUselessPeer) peer.Peer.Disconnect(p2p.DiscUselessPeer)
return err return err
} }

View file

@ -975,6 +975,14 @@ web3._extend({
name: 'syncStatus', name: 'syncStatus',
getter: 'scroll_syncStatus', getter: 'scroll_syncStatus',
}), }),
new web3._extend.Property({
name: 'authorizedSigner',
getter: 'scroll_getAuthorizedSigner',
}),
new web3._extend.Property({
name: 'localSigner',
getter: 'scroll_getLocalSigner',
}),
] ]
}); });
` `

View file

@ -363,7 +363,10 @@ func (w *worker) mainLoop() {
if errors.As(err, &retryableCommitError) || errors.Is(err, system_contract.ErrUnauthorizedSigner) { if errors.As(err, &retryableCommitError) || errors.Is(err, system_contract.ErrUnauthorizedSigner) {
log.Warn("failed to commit to a block, retrying", "err", err) log.Warn("failed to commit to a block, retrying", "err", err)
if errors.Is(err, system_contract.ErrUnauthorizedSigner) { 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. // half the time it takes for the system contract consensus to read and update the address locally.
// note: a blocking wait here might be problematic, since it will prevent progress on
// `updateSnapshot` and other functionalities.
time.Sleep(5 * time.Second)
} }
if _, err = w.tryCommitNewWork(time.Now(), w.current.header.ParentHash, w.current.reorging, w.current.reorgReason); err != nil { if _, err = w.tryCommitNewWork(time.Now(), w.current.header.ParentHash, w.current.reorging, w.current.reorgReason); err != nil {
continue continue
@ -460,6 +463,7 @@ func (w *worker) collectPendingL1Messages(startIndex uint64) []types.L1MessageTx
l1MessagesV1 := rawdb.ReadL1MessagesV1From(w.eth.ChainDb(), startIndex, maxCount) l1MessagesV1 := rawdb.ReadL1MessagesV1From(w.eth.ChainDb(), startIndex, maxCount)
if len(l1MessagesV1) > 0 { if len(l1MessagesV1) > 0 {
// backdate the block to the parent block's timestamp -> not yet EuclidV2 // backdate the block to the parent block's timestamp -> not yet EuclidV2
// TODO: might need to re-run Prepare here
w.current.header.Time = parent.Time w.current.header.Time = parent.Time
return l1MessagesV1 return l1MessagesV1
} }

View file

@ -793,8 +793,12 @@ func (c *ChainConfig) String() string {
switch { switch {
case c.Ethash != nil: case c.Ethash != nil:
engine = c.Ethash engine = c.Ethash
case c.Clique != nil && c.SystemContract != nil:
engine = "upgradable (clique + system_contract)"
case c.Clique != nil: case c.Clique != nil:
engine = c.Clique engine = c.Clique
case c.SystemContract != nil:
engine = c.SystemContract
default: default:
engine = "unknown" engine = "unknown"
} }

View file

@ -24,7 +24,7 @@ import (
const ( const (
VersionMajor = 5 // Major version component of the current release VersionMajor = 5 // Major version component of the current release
VersionMinor = 8 // Minor version component of the current release VersionMinor = 8 // Minor version component of the current release
VersionPatch = 14 // Patch version component of the current release VersionPatch = 15 // Patch version component of the current release
VersionMeta = "mainnet" // Version metadata to append to the version string VersionMeta = "mainnet" // Version metadata to append to the version string
) )