Skip to content

feat: address system contract consensus issues and comments #1126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions consensus/clique/clique.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ type Clique struct {
// New creates a Clique proof-of-authority consensus engine with the initial
// signers set to the ones provided by the user.
func New(config *params.CliqueConfig, db ethdb.Database) *Clique {
log.Info("Initializing clique consensus engine")

// Set any missing consensus parameters to their defaults
conf := *config
if conf.Epoch == 0 {
Expand Down
13 changes: 8 additions & 5 deletions consensus/system_contract/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ 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
system_contract *SystemContract
}

// GetSigners retrieves the list of authorized signers at the specified block.
func (api *API) GetSigners(number *rpc.BlockNumber) ([]common.Address, error) {
return nil, nil
func (api *API) GetLocalSigner() (common.Address, error) {
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
}
21 changes: 14 additions & 7 deletions consensus/system_contract/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,27 @@ 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.
// the previous block's timestamp.
errInvalidTimestamp = errors.New("invalid timestamp")

// errInvalidExtra is returned if the extra data is not empty
errInvalidExtra = errors.New("invalid extra")
)
Expand Down Expand Up @@ -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
// 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)
Expand All @@ -354,8 +357,12 @@ func (s *SystemContract) CalcDifficulty(chain consensus.ChainHeaderReader, time
// controlling the signer voting.
func (s *SystemContract) APIs(chain consensus.ChainHeaderReader) []rpc.API {
return []rpc.API{{
Namespace: "system_contract",
Service: &API{},
// note: cannot use underscore in namespace,
// but overlap with another module's name space works fine.
Namespace: "scroll",
Version: "1.0",
Service: &API{system_contract: s},
Public: false,
}}
}

Expand Down
30 changes: 19 additions & 11 deletions consensus/system_contract/system_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ const (
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 {
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

Expand All @@ -32,8 +33,10 @@ type SystemContract struct {
}

// 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 {
log.Info("Initializing system_contract consensus engine", "config", config)

ctx, cancel := context.WithCancel(ctx)

s := &SystemContract{
Expand All @@ -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
// with.
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()
defer s.lock.Unlock()

Expand Down Expand Up @@ -93,19 +96,17 @@ func (s *SystemContract) fetchAddressFromL1() error {

// Validate the address is not empty
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())

s.lock.RLock()
addressChanged := s.signerAddressL1 != bAddress
s.lock.RUnlock()
s.lock.Lock()
defer s.lock.Unlock()

if addressChanged {
s.lock.Lock()
if s.signerAddressL1 != bAddress {
s.signerAddressL1 = bAddress
s.lock.Unlock()
log.Info("Updated new signer from L1 system contract", "signer", bAddress.Hex())
}

return nil
Expand All @@ -123,3 +124,10 @@ func (s *SystemContract) currentSignerAddressL1() common.Address {

return s.signerAddressL1
}

func (s *SystemContract) localSignerAddress() common.Address {
s.lock.RLock()
defer s.lock.RUnlock()

return s.signer
}
93 changes: 52 additions & 41 deletions consensus/wrapper/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ package wrapper

import (
"math/big"
"sync"
"time"

"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/log"
"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 takes a block timestamp, and returns true once the engine should be upgraded to SystemContract.
isUpgraded func(uint64) bool

// clique is the original Clique consensus engine.
Expand All @@ -28,14 +29,16 @@ type UpgradableEngine struct {

// NewUpgradableEngine constructs a new upgradable consensus middleware.
func NewUpgradableEngine(isUpgraded func(uint64) bool, clique consensus.Engine, system consensus.Engine) *UpgradableEngine {
log.Info("Initializing upgradable consensus engine")

return &UpgradableEngine{
isUpgraded: isUpgraded,
clique: clique,
system: system,
}
}

// 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 {
if ue.isUpgraded(header.Time) {
return ue.system
Expand All @@ -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.
func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) {
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 len(headers) == 0 {
close(out)
return nil, out
close(results)
return nil, results
}

// Choose engine for the first and last header.
Expand Down Expand Up @@ -97,42 +100,55 @@ func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, hea
systemHeaders := headers[splitIndex:]
systemSeals := seals[splitIndex:]

// Create a wait group to merge results.
var wg sync.WaitGroup
wg.Add(2)
log.Info("Verifying EuclidV2 transition header chain")

// 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() {
defer wg.Done()
_, cliqueResults := ue.clique.VerifyHeaders(chain, cliqueHeaders, cliqueSeals)
for err := range cliqueResults {
defer close(results)

// 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 {
case <-abort:
close(abortClique)
log.Warn("Aborted EuclidV2 transition verification in Clique section")
return
case out <- err:
case err := <-cliqueResults:
results <- err
}
}
}()

go func() {
defer wg.Done()
_, systemResults := ue.system.VerifyHeaders(chain, systemHeaders, systemSeals)
for err := range systemResults {
// Not sure why we need this here, but without this we get err="unknown ancestor"
// at the 1st Euclid block. It seems that `VerifyHeaders` start processing the next
// header before the previous one was written into `chain`.
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 {
case <-abort:
close(abortSystem)
log.Info("Aborted EuclidV2 transition verification in SystemContract section")
return
case out <- err:
case err := <-systemResults:
results <- err
}
}
}()

// Close the out channel when both verifications are complete.
go func() {
wg.Wait()
close(out)
log.Info("Completed EuclidV2 transition verification")
}()

return abort, out
return abort, results
}

// Prepare prepares a block header for sealing.
Expand Down Expand Up @@ -167,27 +183,22 @@ func (ue *UpgradableEngine) VerifyUncles(chain consensus.ChainReader, block *typ

// 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)
return append(ue.clique.APIs(chain), ue.system.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
err1 := ue.clique.Close()
err2 := ue.system.Close()

if err1 != nil || err2 != nil {
log.Error("Error while closing upgradable engine", "cliqueError", err1, "systemContractError", err2)
}
if err1 != nil {
return err1
}
return ue.system.Close()
return err2
}

// SealHash returns the hash of a block prior to it being sealed.
Expand Down
15 changes: 12 additions & 3 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,17 @@ 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
// 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"`

// 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"`

// WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.
Expand All @@ -108,7 +115,9 @@ type Header struct {
// 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
// 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:"-"`
}

Expand Down
2 changes: 2 additions & 0 deletions eth/protocols/eth/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ func handleNewBlock(backend Backend, msg Decoder, peer *Peer) error {

hCopy := ann.Block.Header()
if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil {
log.Warn("Disconnecting peer in handleNewBlock", "peer", peer.id, "reason", err)
peer.Peer.Disconnect(p2p.DiscUselessPeer)
return err
}
Expand Down Expand Up @@ -309,6 +310,7 @@ func handleBlockHeaders66(backend Backend, msg Decoder, peer *Peer) error {
for _, header := range res.BlockHeadersPacket {
hCopy := types.CopyHeader(header)
if err := hCopy.NetworkCompatibleEuclidV2Fields(); err != nil {
log.Warn("Disconnecting peer in handleBlockHeaders66", "peer", peer.id, "reason", err)
peer.Peer.Disconnect(p2p.DiscUselessPeer)
return err
}
Expand Down
8 changes: 8 additions & 0 deletions internal/web3ext/web3ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,14 @@ web3._extend({
name: 'syncStatus',
getter: 'scroll_syncStatus',
}),
new web3._extend.Property({
name: 'authorizedSigner',
getter: 'scroll_getAuthorizedSigner',
}),
new web3._extend.Property({
name: 'localSigner',
getter: 'scroll_getLocalSigner',
}),
]
});
`
Loading
Loading