From e3cf555c04f0ff67a2871f8a5a71fabfd47d4ccc Mon Sep 17 00:00:00 2001 From: Nazarii Denha Date: Tue, 5 Nov 2024 08:27:52 +0100 Subject: [PATCH] system contract consensus --- consensus/system_contract/api.go | 7 + consensus/system_contract/consensus.go | 381 +++++++++++++++++++ consensus/system_contract/system_contract.go | 101 +++++ params/config.go | 22 +- 4 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 consensus/system_contract/api.go create mode 100644 consensus/system_contract/consensus.go create mode 100644 consensus/system_contract/system_contract.go diff --git a/consensus/system_contract/api.go b/consensus/system_contract/api.go new file mode 100644 index 000000000000..8dc72aab029f --- /dev/null +++ b/consensus/system_contract/api.go @@ -0,0 +1,7 @@ +package system_contract + + +// API is a user facing RPC API to allow controlling the signer and voting +// mechanisms of the proof-of-authority scheme. +type API struct { +} diff --git a/consensus/system_contract/consensus.go b/consensus/system_contract/consensus.go new file mode 100644 index 000000000000..e04de9676547 --- /dev/null +++ b/consensus/system_contract/consensus.go @@ -0,0 +1,381 @@ +package system_contract + +import ( + "bytes" + "errors" + "fmt" + "io" + "math/big" + "time" + + "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/consensus/misc/eip1559" + "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/params" + "github.com/scroll-tech/go-ethereum/rlp" + "github.com/scroll-tech/go-ethereum/rpc" + "github.com/scroll-tech/go-ethereum/trie" + "golang.org/x/crypto/sha3" +) + +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") + // errCoinbaseNotEmpty is returned if a coinbase value is non-zero + errInvalidCoinbase = errors.New("coinbase not empty nor zero") + // 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-zero 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") + // errUnauthorizedSigner is returned if a header is signed by a non-authorized entity. + 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 extra-data 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) 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) (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]) + + 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 coinbase is zero + if header.Coinbase != (common.Address{}) { + return errInvalidCoinbase + } + // Ensure that the nonce is zero + if header.Nonce != (types.BlockNonce{}) { + return errInvalidNonce + } + // Check that the extra-data contains signature + if len(header.Extra) != 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 zero + if header.Difficulty == nil || header.Difficulty.Cmp(common.Big0) != 0 { + return errInvalidDifficulty + } + // Verify that the gas limit is <= 2^63-1 + if header.GasLimit > params.MaxGasLimit { + return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, params.MaxGasLimit) + } + // if chain.Config().IsShanghai(header.Number, header.Time) { + // return errors.New("clique does not support shanghai fork") + // } + // All basic checks passed, verify cascading fields + return s.verifyCascadingFields(chain, header, parents) +} + +// verifyCascadingFields verifies all the header fields that are not standalone, +// rather depend on a batch of previous headers. The caller may optionally pass +// in a batch of parents (ascending order) to avoid looking those up from the +// database. This is useful for concurrently verifying a batch of new headers. +func (s *SystemContract) verifyCascadingFields(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + // The genesis block is the always valid dead-end + number := header.Number.Uint64() + if number == 0 { + return nil + } + // Ensure that the block's timestamp isn't too close to its parent + var parent *types.Header + if len(parents) > 0 { + parent = parents[len(parents)-1] + } else { + parent = chain.GetHeader(header.ParentHash, number-1) + } + if parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash { + return consensus.ErrUnknownAncestor + } + if header.Time < parent.Time { + return errInvalidTimestamp + } + // Verify that the gasUsed is <= gasLimit + if header.GasUsed > header.GasLimit { + return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit) + } + if !chain.Config().IsCurie(header.Number) { + // Verify BaseFee not present before EIP-1559 fork. + if header.BaseFee != nil { + return fmt.Errorf("invalid baseFee before fork: have %d, want ", header.BaseFee) + } + if err := misc.VerifyGaslimit(parent.GasLimit, header.GasLimit); err != nil { + return err + } + } else if err := eip1559.VerifyEIP1559Header(chain.Config(), parent, header); err != nil { + // Verify the header's EIP-1559 attributes. + return err + } + + signer, err := ecrecover(header) + if err != nil { + return err + } + + s.lock.Lock() + defer s.lock.Unlock() + + if signer != 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.Extra = make([]byte, extraSeal) + // 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()) + } + return nil +} + +// Finalize implements consensus.Engine. There is no post-transaction +// consensus rules in clique, therefore no rules here +func (s *SystemContract) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) { + // 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, withdrawals []*types.Withdrawal) (*types.Block, error) { + if len(withdrawals) > 0 { + return nil, errors.New("system_contract does not support withdrawals") + } + // Finalize block + s.Finalize(chain, header, state, txs, uncles, nil) + + // 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 + s.lock.RUnlock() + + // Bail out if we're unauthorized to sign a block + // todo + + // 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.Extra[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.Extra[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 in clique") + } + if err := rlp.Encode(w, enc); err != nil { + panic("can't encode: " + err.Error()) + } +} diff --git a/consensus/system_contract/system_contract.go b/consensus/system_contract/system_contract.go new file mode 100644 index 000000000000..3fbeddf4e278 --- /dev/null +++ b/consensus/system_contract/system_contract.go @@ -0,0 +1,101 @@ +package system_contract + +import ( + "context" + "math/big" + "sync" + "time" + + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/log" + "github.com/scroll-tech/go-ethereum/params" +) + +const ( + defaultSyncInterval = 10 +) + +// SystemContract +type SystemContract struct { + config *params.SystemContractConfig // Consensus engine configuration parameters + client 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 EthClient) *SystemContract { + blockNumber := big.NewInt(0) // todo: get block number from L1BlocksContract (l1 block hash relay) or other source (depending on exact design) + ctx, cancel := context.WithCancel(ctx) + address, err := client.StorageAt(ctx, config.SystemContractAddress, config.SystemContractSlot, blockNumber) + if err != nil { + // + } + systemContract := &SystemContract{ + config: config, + client: client, + signerAddressL1: common.BytesToAddress(address), + + ctx: ctx, + cancel: cancel, + } + systemContract.Start() + return systemContract +} + +// Authorize injects a private key into the consensus engine to mint new blocks +// with. +func (s *SystemContract) Authorize(signer common.Address, signFn SignerFn) { + s.lock.Lock() + defer s.lock.Unlock() + + s.signer = signer + s.signFn = signFn +} + +func (s *SystemContract) Start() { + log.Info("starting SystemContract") + go func() { + syncTicker := time.NewTicker(defaultSyncInterval) + defer syncTicker.Stop() + for { + select { + case <-s.ctx.Done(): + return + default: + } + select { + case <-s.ctx.Done(): + return + case <-syncTicker.C: + blockNumber := big.NewInt(0) // todo: get block number from L1BlocksContract (l1 block hash relay) or other source (depending on exact design) + + address, err := s.client.StorageAt(s.ctx, s.config.SystemContractAddress, s.config.SystemContractSlot, blockNumber) + if err != nil { + s.lock.Lock() + s.signerAddressL1 = common.BytesToAddress(address) + s.lock.Unlock() + } + } + } + }() +} + +// Close implements consensus.Engine. +func (s *SystemContract) Close() error { + s.cancel() + return nil +} + +type EthClient interface { + StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) + StorageAtHash(ctx context.Context, account common.Address, key common.Hash, blockHash common.Hash) ([]byte, error) +} diff --git a/params/config.go b/params/config.go index 6a81fe990a4d..d669d3870170 100644 --- a/params/config.go +++ b/params/config.go @@ -520,9 +520,10 @@ type ChainConfig struct { TerminalTotalDifficultyPassed bool `json:"terminalTotalDifficultyPassed,omitempty"` // Various consensus engines - Ethash *EthashConfig `json:"ethash,omitempty"` - Clique *CliqueConfig `json:"clique,omitempty"` - IsDevMode bool `json:"isDev,omitempty"` + Ethash *EthashConfig `json:"ethash,omitempty"` + Clique *CliqueConfig `json:"clique,omitempty"` + SystemContract *SystemContractConfig `json"systemContract,omitempty"` + IsDevMode bool `json:"isDev,omitempty"` // Scroll genesis extension: enable scroll rollup-related traces & state transition Scroll ScrollConfig `json:"scroll,omitempty"` @@ -628,6 +629,21 @@ func (c *CliqueConfig) String() string { return "clique" } +// SystemContractConfig is the consensus engine configs for proof-of-work based 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" +} + // Description returns a human-readable description of ChainConfig. func (c *ChainConfig) Description() string { var banner string