Skip to content

Commit

Permalink
proof: verify keys, pre-state and post-state
Browse files Browse the repository at this point in the history
Signed-off-by: Ignacio Hagopian <[email protected]>
  • Loading branch information
jsign committed Oct 26, 2023
1 parent 3f4159b commit 28267aa
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 35 deletions.
53 changes: 44 additions & 9 deletions core/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package core

import (
"bytes"
"fmt"
"math/big"
"sort"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
Expand Down Expand Up @@ -358,32 +360,35 @@ func GenerateChainWithGenesis(genesis *Genesis, engine consensus.Engine, n int,
panic(err)
}
if genesis.Config != nil && genesis.Config.IsPrague(genesis.ToBlock().Number(), genesis.ToBlock().Time()) {
blocks, receipts, _, _ := GenerateVerkleChain(genesis.Config, genesis.ToBlock(), engine, db, n, gen)
blocks, receipts, _, _, _, _, _ := GenerateVerkleChain(genesis.Config, genesis.ToBlock(), engine, db, n, gen)
return db, blocks, receipts
}
blocks, receipts := GenerateChain(genesis.Config, genesis.ToBlock(), engine, db, n, gen)
return db, blocks, receipts
}

func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine consensus.Engine, db ethdb.Database, n int, gen func(int, *BlockGen)) ([]*types.Block, []types.Receipts, []*verkle.VerkleProof, []verkle.StateDiff) {
func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine consensus.Engine, db ethdb.Database, n int, gen func(int, *BlockGen)) ([]*types.Block, []types.Receipts, []*verkle.VerkleProof, []verkle.StateDiff, [][][]byte, [][][]byte, [][][]byte) {
if config == nil {
config = params.TestChainConfig
}
proofs := make([]*verkle.VerkleProof, 0, n)
keyvals := make([]verkle.StateDiff, 0, n)
blocks, receipts := make(types.Blocks, n), make([]types.Receipts, n)
computedKeys := make([][][]byte, n)
computedPreStateValues := make([][][]byte, n)
computedPostStateValues := make([][][]byte, n)
chainreader := &generatedLinearChainReader{
config: config,
// GenerateVerkleChain should only be called with the genesis block
// as parent.
genesis: parent,
chain: blocks,
}
genblock := func(i int, parent *types.Block, statedb *state.StateDB) (*types.Block, types.Receipts) {
genblock := func(i int, parent *types.Block, statedb *state.StateDB) (*types.Block, types.Receipts, [][]byte, [][]byte, [][]byte) {
b := &BlockGen{i: i, chain: blocks, parent: parent, statedb: statedb, config: config, engine: engine}
b.header = makeHeader(chainreader, parent, statedb, b.engine)
preState := statedb.Copy()
fmt.Println("prestate", preState.GetTrie().(*trie.VerkleTrie).ToDot())
preStateTree := statedb.Copy().GetTrie().(*trie.VerkleTrie)
fmt.Println("prestate", preStateTree.ToDot())

// Mutate the state and block according to any hard-fork specs
if daoBlock := config.DAOForkBlock; daoBlock != nil {
Expand All @@ -408,6 +413,19 @@ func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine
panic(err)
}

// Get the keys collected in the witness in the witness.
computedKeys := statedb.Witness().Keys()
sort.Slice(computedKeys, func(i, j int) bool { return bytes.Compare(computedKeys[i], computedKeys[j]) < 0 })

// Get the pre-state values from the database.
computedPreStateValues := make([][]byte, len(computedKeys))
for i := range computedKeys {
computedPreStateValues[i], err = preStateTree.GetWithHashedKey(computedKeys[i])
if err != nil {
panic(err)
}
}

// Write state changes to db
root, err := statedb.Commit(b.header.Number.Uint64(), config.IsEIP158(b.header.Number))
if err != nil {
Expand All @@ -420,9 +438,23 @@ func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine
proofs = append(proofs, block.ExecutionWitness().VerkleProof)
keyvals = append(keyvals, block.ExecutionWitness().StateDiff)

return block, b.receipts
computedPostStateValues := make([][]byte, len(computedKeys))
treeWrites := statedb.GetTrie().(*trie.VerkleTrie).GetTreeWrites()
for i := range computedKeys {
treeWrittenValue, ok := treeWrites[string(computedKeys[i])]
// If we didn't tracked this key, it means it was never written to the post-state tree,
// thus the newValue must be `nil`. (i.e: same as currentValue)
// Additionally, if we wrote to this key but it has the same pre-state value, then must
// also be `nil`.
if !ok || bytes.Equal(treeWrittenValue, computedPreStateValues[i]) {
continue
}
computedPostStateValues[i] = treeWrittenValue
}

return block, b.receipts, computedKeys, computedPreStateValues, computedPostStateValues
}
return nil, nil
return nil, nil, nil, nil, nil
}
var snaps *snapshot.Tree
for i := 0; i < n; i++ {
Expand All @@ -432,13 +464,16 @@ func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine
if err != nil {
panic(fmt.Sprintf("could not find state for block %d: err=%v, parent root=%x", i, err, parent.Root()))
}
block, receipt := genblock(i, parent, statedb)
block, receipt, keys, preStateValues, postStateValues := genblock(i, parent, statedb)
blocks[i] = block
receipts[i] = receipt
computedKeys[i] = keys
computedPreStateValues[i] = preStateValues
computedPostStateValues[i] = postStateValues
parent = block
snaps = statedb.Snaps()
}
return blocks, receipts, proofs, keyvals
return blocks, receipts, proofs, keyvals, computedKeys, computedPreStateValues, computedPostStateValues
}

func makeHeader(chain consensus.ChainReader, parent *types.Block, state *state.StateDB, engine consensus.Engine) *types.Header {
Expand Down
13 changes: 8 additions & 5 deletions core/state_processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package core

import (
//"bytes"

"crypto/ecdsa"

//"fmt"
Expand Down Expand Up @@ -490,7 +491,7 @@ func TestProcessVerkle(t *testing.T) {
txCost1*2 + txCost2 + contractCreationCost + codeWithExtCodeCopyGas,
}
// TODO utiliser GenerateChainWithGenesis pour le rendre plus pratique
chain, _, proofs, keyvals := GenerateVerkleChain(gspec.Config, genesis, beacon.New(ethash.NewFaker()), gendb, 2, func(i int, gen *BlockGen) {
chain, _, proofs, statediff, computedKeys, computedPreStateValues, computedPostStateValues := GenerateVerkleChain(gspec.Config, genesis, beacon.New(ethash.NewFaker()), gendb, 2, func(i int, gen *BlockGen) {
gen.SetPoS()
// TODO need to check that the tx cost provided is the exact amount used (no remaining left-over)
tx, _ := types.SignTx(types.NewTransaction(uint64(i)*3, common.Address{byte(i), 2, 3}, big.NewInt(999), txCost1, big.NewInt(875000000), nil), signer, testKey)
Expand Down Expand Up @@ -518,11 +519,13 @@ func TestProcessVerkle(t *testing.T) {
//f.Write(buf.Bytes())
//fmt.Printf("root= %x\n", chain[0].Root())
// check the proof for the last block
err := trie.DeserializeAndVerifyVerkleProof(proofs[1], chain[0].Root().Bytes(), chain[1].Root().Bytes(), keyvals[1])
if err != nil {
t.Fatal(err)

for i := 1; i < len(proofs); i++ {
if err := trie.DeserializeAndVerifyVerkleProof(proofs[i], statediff[i], chain[i-1].Root().Bytes(), computedKeys[i], computedPreStateValues[i], computedPostStateValues[i]); err != nil {
t.Fatal(err)
}
t.Log("verfied verkle proof")
}
t.Log("verfied verkle proof")

endnum, err := blockchain.InsertChain(chain)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion miner/worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (b *testWorkerBackend) newRandomVerkleUncle() *types.Block {
} else {
parent = b.chain.GetBlockByHash(b.chain.CurrentBlock().ParentHash)
}
blocks, _, _, _ := core.GenerateVerkleChain(b.chain.Config(), parent, b.chain.Engine(), b.db, 1, func(i int, gen *core.BlockGen) {
blocks, _, _, _, _ := core.GenerateVerkleChain(b.chain.Config(), parent, b.chain.Engine(), b.db, 1, func(i int, gen *core.BlockGen) {

Check failure on line 161 in miner/worker_test.go

View workflow job for this annotation

GitHub Actions / test

assignment mismatch: 5 variables but core.GenerateVerkleChain returns 7 values

Check failure on line 161 in miner/worker_test.go

View workflow job for this annotation

GitHub Actions / lint

assignment mismatch: 5 variables but core.GenerateVerkleChain returns 7 values (typecheck)
var addr = make([]byte, common.AddressLength)
rand.Read(addr)
gen.SetCoinbase(common.BytesToAddress(addr))
Expand Down
104 changes: 84 additions & 20 deletions trie/verkle.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type VerkleTrie struct {
db *Database
pointCache *utils.PointCache
ended bool

treeWrites map[string][]byte
}

func (vt *VerkleTrie) ToDot() string {
Expand All @@ -51,6 +53,7 @@ func NewVerkleTrie(root verkle.VerkleNode, db *Database, pointCache *utils.Point
db: db,
pointCache: pointCache,
ended: ended,
treeWrites: make(map[string][]byte),
}
}

Expand All @@ -59,6 +62,7 @@ func (trie *VerkleTrie) FlatdbNodeResolver(path []byte) ([]byte, error) {
}

func (trie *VerkleTrie) InsertMigratedLeaves(leaves []verkle.LeafNode) error {
// Note: these values intentionally not inserted in the postValues map.
return trie.root.(*verkle.InternalNode).InsertMigratedLeaves(leaves, trie.FlatdbNodeResolver)
}

Expand Down Expand Up @@ -185,16 +189,22 @@ func (t *VerkleTrie) UpdateAccount(addr common.Address, acc *types.StateAccount)
}
// TODO figure out if the code size needs to be updated, too

t.trackPostStateValues(stem, values)

return nil
}

func (trie *VerkleTrie) UpdateStem(key []byte, values [][]byte) error {
func (trie *VerkleTrie) UpdateStem(stem []byte, values [][]byte) error {
switch root := trie.root.(type) {
case *verkle.InternalNode:
return root.InsertStem(key, values, trie.FlatdbNodeResolver)
if err := root.InsertStem(stem, values, trie.FlatdbNodeResolver); err != nil {
return fmt.Errorf("updating stem: %v", err)
}
trie.trackPostStateValues(stem, values)
default:
panic("invalid root type")
}
return nil
}

// Update associates key with value in the trie. If value has length zero, any
Expand All @@ -209,7 +219,13 @@ func (trie *VerkleTrie) UpdateStorage(address common.Address, key, value []byte)
} else {
copy(v[32-len(value):], value[:])
}
return trie.root.Insert(k, v[:], trie.FlatdbNodeResolver)
if err := trie.root.Insert(k, v[:], trie.FlatdbNodeResolver); err != nil {
return fmt.Errorf("inserting key: %s", err)
}

trie.treeWrites[string(k)] = v[:]

return nil
}

func (t *VerkleTrie) DeleteAccount(addr common.Address) error {
Expand All @@ -234,6 +250,8 @@ func (t *VerkleTrie) DeleteAccount(addr common.Address) error {
}
// TODO figure out if the code size needs to be updated, too

t.trackPostStateValues(stem, values)

return nil
}

Expand All @@ -243,7 +261,12 @@ func (trie *VerkleTrie) DeleteStorage(addr common.Address, key []byte) error {
pointEval := trie.pointCache.GetTreeKeyHeader(addr[:])
k := utils.GetTreeKeyStorageSlotWithEvaluatedAddress(pointEval, key)
var zero [32]byte
return trie.root.Insert(k, zero[:], trie.FlatdbNodeResolver)
if err := trie.root.Insert(k, zero[:], trie.FlatdbNodeResolver); err != nil {
return fmt.Errorf("inserting key: %s", err)
}
trie.treeWrites[string(k)] = zero[:]

return nil
}

// Hash returns the root hash of the trie. It does not write to the database and
Expand Down Expand Up @@ -306,6 +329,13 @@ func (trie *VerkleTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
panic("not implemented")
}

// GetTreeWrites returns the a map that contains the addresses and values that were
// written to the trie. The returned map **is not** a copy, so any mutation to it
// can affect further calls. It's recommended to treat it as read-only.
func (trie *VerkleTrie) GetTreeWrites() map[string][]byte {
return trie.treeWrites
}

func (trie *VerkleTrie) Copy() *VerkleTrie {
return &VerkleTrie{
root: trie.root.Copy(),
Expand Down Expand Up @@ -336,14 +366,48 @@ func ProveAndSerialize(pretrie, posttrie *VerkleTrie, keys [][]byte, resolver ve
return p, kvps, nil
}

func DeserializeAndVerifyVerkleProof(vp *verkle.VerkleProof, preStateRoot []byte, postStateRoot []byte, statediff verkle.StateDiff) error {
// TODO: check that `OtherStems` have expected length and values.

func DeserializeAndVerifyVerkleProof(
vp *verkle.VerkleProof,
statediff verkle.StateDiff,
preStateRoot []byte,
computedKeys [][]byte,
computedPreStateValues [][]byte,
computedPostStateValues [][]byte) error {
proof, err := verkle.DeserializeProof(vp, statediff)
if err != nil {
return fmt.Errorf("verkle proof deserialization error: %w", err)
}

// Verify the provided `statediff` by checking that the keys, pre-values and post-values match exactly
// with the ones provided from the EVM block execution witness.
if len(computedKeys) != len(proof.Keys) {
return fmt.Errorf("witness keys length doesn't match proof keys length: expected %d, got %d", len(computedKeys), len(proof.Keys))
}
for i := range computedKeys {
if !bytes.Equal(computedKeys[i], proof.Keys[i]) {
return fmt.Errorf("witness keys don't match proof keys: expected %x, got %x", computedKeys[i], proof.Keys[i])
}
}
if len(computedPreStateValues) != len(proof.PreValues) {
return fmt.Errorf("witness pre-values length doesn't match proof pre-values length: expected %d, got %d", len(computedPreStateValues), len(proof.PreValues))
}
for i := range computedPreStateValues {
if !bytes.Equal(computedPreStateValues[i], proof.PreValues[i]) {
return fmt.Errorf("witness pre-values don't match proof pre-values: expected %x, got %x", computedPreStateValues[i], proof.PreValues[i])
}
}
if len(computedPostStateValues) != len(proof.PostValues) {
return fmt.Errorf("witness post-values length doesn't match proof post-values length: expected %d, got %d", len(computedPostStateValues), len(proof.PostValues))

}
for i := range computedPostStateValues {
if !bytes.Equal(computedPostStateValues[i], proof.PostValues[i]) {
return fmt.Errorf("witness post-values don't match proof post-values: expected %x, got %x", computedPostStateValues[i], proof.PostValues[i])
}
}

// At the point we know that the pre and post values are correct, we we proceed with reconstructing
// the pre-state tree, and getting the elements to verify the cryptographic proof.
rootC := new(verkle.Point)
rootC.SetBytes(preStateRoot)
pretree, err := verkle.PreStateTreeFromProof(proof, rootC)
Expand Down Expand Up @@ -374,19 +438,6 @@ func DeserializeAndVerifyVerkleProof(vp *verkle.VerkleProof, preStateRoot []byte
}
}

// TODO: this is necessary to verify that the post-values are the correct ones.
// But all this can be avoided with a even faster way. The EVM block execution can
// keep track of the written keys, and compare that list with this post-values list.
// This can avoid regenerating the post-tree which is somewhat expensive.
posttree, err := verkle.PostStateTreeFromStateDiff(pretree, statediff)
if err != nil {
return fmt.Errorf("error rebuilding the post-tree from proof: %w", err)
}
regeneratedPostTreeRoot := posttree.Commitment().Bytes()
if !bytes.Equal(regeneratedPostTreeRoot[:], postStateRoot) {
return fmt.Errorf("post tree root mismatch: %x != %x", regeneratedPostTreeRoot, postStateRoot)
}

return verkle.VerifyVerkleProofWithPreState(proof, pretree)
}

Expand Down Expand Up @@ -496,3 +547,16 @@ func (t *VerkleTrie) UpdateContractCode(addr common.Address, codeHash common.Has
}
return nil
}

func (trie *VerkleTrie) trackPostStateValues(stem []byte, values [][]byte) {
addr := make([]byte, verkle.StemSize+1)
copy(addr[:verkle.StemSize], stem)
for i := range values {
if len(values[i]) == 0 {
continue
}
addr[verkle.StemSize] = byte(i)
fmt.Printf("Tracking %x: %x\n", string(addr), values[i])
trie.treeWrites[string(addr)] = values[i]
}
}

0 comments on commit 28267aa

Please sign in to comment.