Skip to content
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

blockchain: add execution witness validation #515

Merged
merged 30 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 21 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: 1 addition & 1 deletion .github/workflows/stable-spec-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:

env:
FIXTURES_TAG: "verkle@v0.0.6"
FIXTURES_TAG: "verkle@v0.0.7-alpha-1"
jsign marked this conversation as resolved.
Show resolved Hide resolved

jobs:
setup:
Expand Down
42 changes: 25 additions & 17 deletions cmd/evm/internal/t8ntool/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type ExecutionResult struct {
// Verkle witness
VerkleProof *verkle.VerkleProof `json:"verkleProof,omitempty"`
StateDiff verkle.StateDiff `json:"stateDiff,omitempty"`
ParentRoot common.Hash `json:"parentRoot,omitempty"`

// Values to test the verkle conversion
CurrentAccountAddress *common.Address `json:"currentConversionAddress,omitempty" gencodec:"optional"`
Expand Down Expand Up @@ -163,17 +164,18 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
return h
}
var (
statedb = MakePreState(rawdb.NewMemoryDatabase(), chainConfig, pre, chainConfig.IsVerkle(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp))
vtrpre *trie.VerkleTrie
signer = types.MakeSigner(chainConfig, new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp)
gaspool = new(core.GasPool)
blockHash = common.Hash{0x13, 0x37}
rejectedTxs []*rejectedTx
includedTxs types.Transactions
gasUsed = uint64(0)
receipts = make(types.Receipts, 0)
txIndex = 0
parentRoot, statedb = MakePreState(rawdb.NewMemoryDatabase(), chainConfig, pre, chainConfig.IsVerkle(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp))
jsign marked this conversation as resolved.
Show resolved Hide resolved
vtrpre *trie.VerkleTrie
signer = types.MakeSigner(chainConfig, new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp)
gaspool = new(core.GasPool)
blockHash = common.Hash{0x13, 0x37}
rejectedTxs []*rejectedTx
includedTxs types.Transactions
gasUsed = uint64(0)
receipts = make(types.Receipts, 0)
txIndex = 0
)

gaspool.AddGas(pre.Env.GasLimit)
vmContext := vm.BlockContext{
CanTransfer: core.CanTransfer,
Expand All @@ -191,8 +193,6 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
switch tr := statedb.GetTrie().(type) {
case *trie.VerkleTrie:
vtrpre = tr.Copy()
case *trie.TransitionTrie:
vtrpre = tr.Overlay().Copy()
Comment on lines -194 to -195
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We stop doing witness generation for Overlay Tree.

}

// If currentBaseFee is defined, add it to the vmContext.
Expand Down Expand Up @@ -391,6 +391,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
BaseFee: (*math.HexOrDecimal256)(vmContext.BaseFee),
VerkleProof: vktProof,
StateDiff: vktStateDiff,
ParentRoot: parentRoot,
}
if pre.Env.Withdrawals != nil {
h := types.DeriveSha(types.Withdrawals(pre.Env.Withdrawals), trie.NewStackTrie(nil))
Expand Down Expand Up @@ -425,7 +426,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
return statedb, execRs, nil
}

func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prestate, verkle bool) *state.StateDB {
func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prestate, verkle bool) (common.Hash, *state.StateDB) {
// Start with generating the MPT DB, which should be empty if it's post-verkle transition
sdb := state.NewDatabaseWithConfig(db, &trie.Config{Preimages: true, Verkle: false})

Expand All @@ -451,9 +452,12 @@ func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prest
codeHash := crypto.Keccak256Hash(acc.Code)
rawdb.WriteCode(codeWriter, codeHash, acc.Code)
}
statedb.Commit(0, false)
root, err := statedb.Commit(0, false)
if err != nil {
panic(err)
}
Comment on lines +455 to +458
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from collecting the root, we were also ignoring a potential error.


return statedb
return root, statedb
}

// MPT pre is the same as the pre state for first conversion block
Expand All @@ -473,12 +477,13 @@ func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prest
panic(err)
}

parentRoot := mptRoot
// If verkle mode started, establish the conversion
if verkle {
// If the current tree is a VerkleTrie, it means the state conversion has ended.
// We don't need to continue with conversion setups and can return early.
if _, ok := statedb.GetTrie().(*trie.VerkleTrie); ok {
return statedb
return parentRoot, statedb
}

rawdb.WritePreimages(sdb.DiskDB(), statedb.Preimages())
Expand Down Expand Up @@ -548,6 +553,9 @@ func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prest
}

root, _ := statedb.Commit(0, false)
if pre.VKT != nil {
parentRoot = root
}

// recreate the verkle db with the tree root, but this time with the mpt snapshot,
// so that the conversion can proceed.
Expand All @@ -565,7 +573,7 @@ func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prest
if statedb.Database().InTransition() || statedb.Database().Transitioned() {
log.Info("at end of makestate", "in transition", statedb.Database().InTransition(), "during", statedb.Database().Transitioned(), "account hash", statedb.Database().GetCurrentAccountHash())
}
return statedb
return parentRoot, statedb
}

func rlpHash(x interface{}) (h common.Hash) {
Expand Down
13 changes: 8 additions & 5 deletions cmd/evm/internal/t8ntool/transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func Transition(ctx *cli.Context) error {
)
// Figure out the prestate alloc
if allocStr == stdinSelector || vktStr == stdinSelector || envStr == stdinSelector || txStr == stdinSelector {
// f, _ := os.Open("/home/ignacio/code/execution-spec-tests/fxtest/shanghai__eip3855_push0__test_push0__test_push0_contract_during_call_contexts/fork_EIP6800Transition_blockchain_test_call/0/stdin.txt")
// decoder := json.NewDecoder(f)
decoder := json.NewDecoder(os.Stdin)
if err := decoder.Decode(inputData); err != nil {
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling stdin: %v", err))
Expand Down Expand Up @@ -322,7 +324,7 @@ func Transition(ctx *cli.Context) error {
vktleaves = make(map[common.Hash]hexutil.Bytes)
s.DumpVKTLeaves(vktleaves)
}
return dispatchOutput(ctx, baseDir, result, collector, vktleaves, body, result.VerkleProof, result.StateDiff)
return dispatchOutput(ctx, baseDir, result, collector, vktleaves, body, result.VerkleProof, result.StateDiff, result.ParentRoot)
}

// txWithKey is a helper-struct, to allow us to use the types.Transaction along with
Expand Down Expand Up @@ -447,7 +449,7 @@ func saveFile(baseDir, filename string, data interface{}) error {

// dispatchOutput writes the output data to either stderr or stdout, or to the specified
// files
func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, alloc Alloc, vkt map[common.Hash]hexutil.Bytes, body hexutil.Bytes, p *verkle.VerkleProof, k verkle.StateDiff) error {
func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, alloc Alloc, vkt map[common.Hash]hexutil.Bytes, body hexutil.Bytes, p *verkle.VerkleProof, k verkle.StateDiff, proot common.Hash) error {
stdOutObject := make(map[string]interface{})
stdErrObject := make(map[string]interface{})
dispatch := func(baseDir, fName, name string, obj interface{}) error {
Expand Down Expand Up @@ -481,9 +483,10 @@ func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, a
}
if p != nil {
if err := dispatch(baseDir, ctx.String(OutputWitnessFlag.Name), "witness", struct {
Proof *verkle.VerkleProof
Diff verkle.StateDiff
}{p, k}); err != nil {
Proof *verkle.VerkleProof
Diff verkle.StateDiff
ParentRoot common.Hash
}{p, k, proot}); err != nil {
return err
}
}
Expand Down
122 changes: 66 additions & 56 deletions consensus/beacon/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,82 +400,92 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
state.Database().SaveTransitionState(header.Root)

var (
p *verkle.VerkleProof
k verkle.StateDiff
keys = state.Witness().Keys()
proot common.Hash
proof *verkle.VerkleProof
stateDiff verkle.StateDiff
proot common.Hash
)
if chain.Config().IsVerkle(header.Number, header.Time) {
// Open the pre-tree to prove the pre-state against
parent := chain.GetHeaderByNumber(header.Number.Uint64() - 1)
if parent == nil {
return nil, fmt.Errorf("nil parent header for block %d", header.Number)
}
proot = parent.Root

// Load transition state at beginning of block, because
// OpenTrie needs to know what the conversion status is.
state.Database().LoadTransitionState(parent.Root)

if chain.Config().ProofInBlocks {
preTrie, err := state.Database().OpenTrie(parent.Root)
if err != nil {
return nil, fmt.Errorf("error opening pre-state tree root: %w", err)
}

var okpre, okpost bool
var vtrpre, vtrpost *trie.VerkleTrie
switch pre := preTrie.(type) {
case *trie.VerkleTrie:
vtrpre, okpre = preTrie.(*trie.VerkleTrie)
switch tr := state.GetTrie().(type) {
case *trie.VerkleTrie:
vtrpost = tr
okpost = true
// This is to handle a situation right at the start of the conversion:
// the post trie is a transition tree when the pre tree is an empty
// verkle tree.
case *trie.TransitionTrie:
vtrpost = tr.Overlay()
okpost = true
default:
okpost = false
}
case *trie.TransitionTrie:
vtrpre = pre.Overlay()
okpre = true
post, _ := state.GetTrie().(*trie.TransitionTrie)
vtrpost = post.Overlay()
okpost = true
default:
// This should only happen for the first block of the
// conversion, when the previous tree is a merkle tree.
// Logically, the "previous" verkle tree is an empty tree.
okpre = true
vtrpre = trie.NewVerkleTrie(verkle.New(), state.Database().TrieDB(), utils.NewPointCache(), false)
post := state.GetTrie().(*trie.TransitionTrie)
vtrpost = post.Overlay()
okpost = true
}
if okpre && okpost {
if len(keys) > 0 {
p, k, err = trie.ProveAndSerialize(vtrpre, vtrpost, keys, vtrpre.FlatdbNodeResolver)
if err != nil {
return nil, fmt.Errorf("error generating verkle proof for block %d: %w", header.Number, err)
}
}
}
Comment on lines -420 to -467
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deleted block chunk is only extracted to a new method below. I did this because now apart from creating witnesses in block generation, we also create the witness in block processing... validating the witness is now part of the block validation (more on that below).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at 517, this function will be split back into smaller elements. I suggest not doing that.

var err error
stateDiff, proof, err = BuildVerkleProof(header, state, parent.Root)
if err != nil {
return nil, fmt.Errorf("error building verkle proof: %w", err)
}
proot = parent.Root
}

// Assemble and return the final block.
block := types.NewBlockWithWithdrawals(header, txs, uncles, receipts, withdrawals, trie.NewStackTrie(nil))
if chain.Config().IsVerkle(header.Number, header.Time) && chain.Config().ProofInBlocks {
block.SetVerkleProof(p, k, proot)
block.SetVerkleProof(proof, stateDiff, proot)
}
return block, nil
}

func BuildVerkleProof(header *types.Header, state *state.StateDB, parentRoot common.Hash) (verkle.StateDiff, *verkle.VerkleProof, error) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is just the old code. No code lines where changed.

var (
proof *verkle.VerkleProof
stateDiff verkle.StateDiff
)

preTrie, err := state.Database().OpenTrie(parentRoot)
if err != nil {
return nil, nil, fmt.Errorf("error opening pre-state tree root: %w", err)
}

var okpre, okpost bool
var vtrpre, vtrpost *trie.VerkleTrie
switch pre := preTrie.(type) {
case *trie.VerkleTrie:
vtrpre, okpre = preTrie.(*trie.VerkleTrie)
switch tr := state.GetTrie().(type) {
case *trie.VerkleTrie:
vtrpost = tr
okpost = true
// This is to handle a situation right at the start of the conversion:
// the post trie is a transition tree when the pre tree is an empty
// verkle tree.
case *trie.TransitionTrie:
vtrpost = tr.Overlay()
okpost = true
default:
okpost = false
}
case *trie.TransitionTrie:
vtrpre = pre.Overlay()
okpre = true
post, _ := state.GetTrie().(*trie.TransitionTrie)
vtrpost = post.Overlay()
okpost = true
default:
// This should only happen for the first block of the
// conversion, when the previous tree is a merkle tree.
// Logically, the "previous" verkle tree is an empty tree.
okpre = true
vtrpre = trie.NewVerkleTrie(verkle.New(), state.Database().TrieDB(), utils.NewPointCache(), false)
post := state.GetTrie().(*trie.TransitionTrie)
vtrpost = post.Overlay()
okpost = true
}
if okpre && okpost {
keys := state.Witness().Keys()
if len(keys) > 0 {
proof, stateDiff, err = trie.ProveAndSerialize(vtrpre, vtrpost, keys, vtrpre.FlatdbNodeResolver)
if err != nil {
return nil, nil, fmt.Errorf("error generating verkle proof for block %d: %w", header.Number, err)
}
}
}
return stateDiff, proof, nil
}

// Seal generates a new sealing request for the given input block and pushes
// the result into the given channel.
//
Expand Down
19 changes: 19 additions & 0 deletions core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"

"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
Expand Down Expand Up @@ -131,6 +132,24 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error())
}
if blockEw := block.ExecutionWitness(); blockEw != nil {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally, we reached the main goal of the PR. ValidateState is an existing method that has the usual stateRoot and gasUsage checks when validating blocks.

Now I added logic to also check the execution witness (if the proposed block has one).

parent := v.bc.GetHeaderByNumber(header.Number.Uint64() - 1)
if parent == nil {
return fmt.Errorf("nil parent header for block %d", header.Number)
}
stateDiff, proof, err := beacon.BuildVerkleProof(header, statedb, parent.Root)
if err != nil {
return fmt.Errorf("error building verkle proof: %w", err)
}
ew := types.ExecutionWitness{
StateDiff: stateDiff,
VerkleProof: proof,
ParentStateRoot: parent.Root,
}
if err := ew.Equal(blockEw); err != nil {
return fmt.Errorf("invalid execution witness: %v", err)
}
}
// Verify that the advertised root is correct before
// it can be used as an identifier for the conversion
// status.
Expand Down
2 changes: 1 addition & 1 deletion core/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine
proots = append(proots, parent.Root())

// quick check that we are self-consistent
err = trie.DeserializeAndVerifyVerkleProof(block.ExecutionWitness().VerkleProof, block.ExecutionWitness().ParentStateRoot[:], block.Root().Bytes(), block.ExecutionWitness().StateDiff)
err = verkle.Verify(block.ExecutionWitness().VerkleProof, block.ExecutionWitness().ParentStateRoot[:], block.Root().Bytes(), block.ExecutionWitness().StateDiff)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sideffects of upgrading go-verkle.

if err != nil {
panic(err)
}
Expand Down
5 changes: 3 additions & 2 deletions core/state_processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie/utils"
"github.com/ethereum/go-verkle"

//"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
Expand Down Expand Up @@ -608,13 +609,13 @@ func TestProcessVerkle(t *testing.T) {
//fmt.Printf("root= %x\n", chain[0].Root())

// check the proof for the 1st block
err = trie.DeserializeAndVerifyVerkleProof(proofs[0], genesis.Root().Bytes(), chain[0].Root().Bytes(), keyvals[0])
err = verkle.Verify(proofs[0], genesis.Root().Bytes(), chain[0].Root().Bytes(), keyvals[0])
gballet marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
spew.Dump(genesis.Root().Bytes(), proofs[0])
t.Fatal(err)
}
// check the proof for the last block
err = trie.DeserializeAndVerifyVerkleProof(proofs[1], chain[0].Root().Bytes(), chain[1].Root().Bytes(), keyvals[1])
err = verkle.Verify(proofs[1], chain[0].Root().Bytes(), chain[1].Root().Bytes(), keyvals[1])
if err != nil {
t.Fatal(err)
}
Expand Down
Loading
Loading