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

t8n: verkle genesis #466

Merged
merged 27 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
118 changes: 118 additions & 0 deletions .github/workflows/spec-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Execution Spec Tests Fill and Consume (Verkle genesis & conversion)

on:
push:
branches: [master]
pull_request:
branches: [master, kaustinen-with-shapella]
workflow_dispatch:

env:
EEST_USER: "ethereum"
EEST_BRANCH: "verkle/main"
jsign marked this conversation as resolved.
Show resolved Hide resolved

jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: Checkout go-ethereum
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12.4"

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.4

- name: Build geth evm
run: |
go build -v ./cmd/evm
mkdir -p ${{ github.workspace }}/bin
mv evm ${{ github.workspace }}/bin/evm
chmod +x ${{ github.workspace }}/bin/evm

- name: Archive built evm
uses: actions/upload-artifact@v4
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Each run uploads the generated fixtures as artifacts. For example, see the bottom of this run.

As we chatted in TG, after everything is stabilized we should probably:

  • Make these filling CI run once per day or similar, instead of on every PR since it takes a long time to run and that can be annoying.
  • The CI to be run on each PR, should be only the "consumption" of fixed (i.e: already generated) fixtures. Running fixtures is quite fast, and also it makes sense to target a stable/tagged set of fixtures. (i.e: filling fixtures on the same branch that consume them always has a high chance of always passing. The main idea is consuming against previous fixtures to catch regressions).

In any case, we can chat when we get a stable set of fixtures and we can change a bit the CI jobs.

with:
name: evm
path: ${{ github.workspace }}/bin/evm

fill:
runs-on: ubuntu-latest
needs: setup
strategy:
matrix:
test-type: [genesis, conversion]
steps:
- name: Download geth evm
uses: actions/download-artifact@v4
with:
name: evm
path: ./bin

- name: Make evm binary executable and add to PATH
run: |
chmod +x ./bin/evm
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH

- name: Clone execution-spec-tests and fill tests
run: |
git clone https://github.com/${{ env.EEST_USER }}/execution-spec-tests -b ${{ env.EEST_BRANCH }}
cd execution-spec-tests
python3 -m venv venv
. venv/bin/activate
pip install --upgrade pip
pip install -e ".[docs,lint,test]"
solc-select use 0.8.24 --always-install
if [ "${{ matrix.test-type }}" == "genesis" ]; then
fill --fork Verkle --output=../fixtures-${{ matrix.test-type }} -v -m blockchain_test -n auto
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

With this line we generate all "targetable" tests on a Verkle fork, that is, using a VKT for the tree which includes:

  • All the new verkle-genesis test vectors I created.
  • All existing tests previously that could be run on a VKT (i.e: tests created from Shanghai or even previous ones).

These means around 458 filled test-vectors using verkle trees.

else
fill --from Shanghai --until EIP6800Transition --output=../fixtures-${{ matrix.test-type }} -v -m blockchain_test -n auto
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here the single state-conversion test vector is filled since we target EIP6800Transition. But also we try to fill all existing tests from Shanghai forward; this isn't strictly useful for "Verkle needs" but we're just making sure filling for previous forks keeps working (and in fact, I had to fix things in t8n to make this work again).

This fills ~605 tests. Most of them aren't entirely useful but only the state-conversion one. The usefulness of consuming those fixtures, is making sure the EL client can keep passing tests for previous forks (e.g: full sync isn't broken).

The more test the better, but probably the other 458 fixtures are much more important to catch Verkle bugs for devnet7.

fi
shell: bash

- name: Upload fixtures
uses: actions/upload-artifact@v4
with:
name: fixtures-${{ matrix.test-type }}
path: fixtures-${{ matrix.test-type }}

consume:
runs-on: ubuntu-latest
needs: fill
strategy:
matrix:
test-type: [genesis, conversion]
steps:
- name: Download geth evm
uses: actions/download-artifact@v4
with:
name: evm
path: ./bin

- name: Make evm binary executable and add to PATH
run: |
chmod +x ./bin/evm
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH

- name: Download fixtures
uses: actions/download-artifact@v4
with:
name: fixtures-${{ matrix.test-type }}
path: ./fixtures-${{ matrix.test-type }}

- name: Clone execution-spec-tests and consume tests
run: |
git clone https://github.com/${{ env.EEST_USER }}/execution-spec-tests -b ${{ env.EEST_BRANCH }}
cd execution-spec-tests
python3 -m venv venv
. venv/bin/activate
pip install --upgrade pip
pip install -e ".[docs,lint,test]"
solc-select use 0.8.24 --always-install
consume direct --input=../fixtures-${{ matrix.test-type }} -n auto
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Consume all fixtures. As I mentioned in other place, I found a problem in consume direct which doesn't return !=0 if any test fails, so the CI doesn't "fail" of some test fails.

That's quite bad since that's the hole point of the CI, but Spencer will work on that to fix it. Until that happens, it might be good to "eyeball" the CI run logs and see all passed.

shell: bash
2 changes: 1 addition & 1 deletion cmd/evm/blockrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func blockTestCmd(ctx *cli.Context) error {
return err
}
for i, test := range tests {
if err := test.Run(false, tracer); err != nil {
if err := test.Run(true, tracer); err != 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.

This is something I discovered while trying to fix running the fixture for the state-conversion. This boolean indicates if the run should enable snapshot, which we need for state-conversion (since we iterate on it).

This will also be true for verkle-genesis tests (since this true isn't conditional). Verkle-genesis test doesn't really need snapshot, but I preferred not to add any extra logic here to make it conditional since that would require a deeper refactor. And also, enabling snapshots for verkle-genesis should still be valid.

return fmt.Errorf("test %v: %w", i, err)
}
}
Expand Down
66 changes: 51 additions & 15 deletions cmd/evm/internal/t8ntool/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,16 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
GasLimit: pre.Env.GasLimit,
GetHash: getHash,
}
// Save pre verkle tree to build the proof at the end
if pre.VKT != nil && len(pre.VKT) > 0 {
switch tr := statedb.GetTrie().(type) {
case *trie.VerkleTrie:
vtrpre = tr.Copy()
case *trie.TransitionTrie:
vtrpre = tr.Overlay().Copy()
default:
panic("invalid trie type")
}

// We save the current state of the Verkle Tree before applying the transactions.
// Note that if the Verkle fork isn't active, this will be a noop.
Comment on lines +189 to +190
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added comment on why we removed the default.

switch tr := statedb.GetTrie().(type) {
case *trie.VerkleTrie:
vtrpre = tr.Copy()
case *trie.TransitionTrie:
vtrpre = tr.Overlay().Copy()
}

// If currentBaseFee is defined, add it to the vmContext.
if pre.Env.BaseFee != nil {
vmContext.BaseFee = new(big.Int).Set(pre.Env.BaseFee)
Expand Down Expand Up @@ -428,8 +427,33 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prestate, verkle bool) *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})

if pre.Env.Ended != nil && *pre.Env.Ended {
sdb.InitTransitionStatus(true, true, common.Hash{})
}

statedb, _ := state.New(types.EmptyRootHash, sdb, nil)

if pre.Env.Ended != nil && *pre.Env.Ended {
vtr := statedb.GetTrie().(*trie.VerkleTrie)

// create the vkt, should be empty on first insert
for k, v := range pre.VKT {
values := make([][]byte, 256)
values[k[31]] = make([]byte, 32)
copy(values[k[31]], v)
vtr.UpdateStem(k.Bytes(), values)
}

codeWriter := statedb.Database().DiskDB()
for _, acc := range pre.Pre {
codeHash := crypto.Keccak256Hash(acc.Code)
rawdb.WriteCode(codeWriter, codeHash, acc.Code)
}

return statedb
}

// MPT pre is the same as the pre state for first conversion block
for addr, a := range pre.Pre {
statedb.SetCode(addr, a.Code)
Expand All @@ -440,14 +464,21 @@ func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prest
}
}

state.NoBanner()
// Commit db an create a snapshot from it.
mptRoot, err := statedb.Commit(0, false)
if err != nil {
panic(err)
}
Comment on lines +467 to +472
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Moving this from inside the if is required, when I was trying to make also the fixture consumption to work for forks previous to Verkle and the transition. (e.g: Shanghai).

The problem was we didn't have the else statement I added in L556. Which meant that we didn't return a correct statedb for that case and nothing worked.

To avoid code repetition, both for the if and else case we need to execute this logic thus I moved it out.


// If verkle mode started, establish the conversion
if verkle {
state.NoBanner()
// Commit db an create a snapshot from it.
mptRoot, err := statedb.Commit(0, false)
if err != nil {
panic(err)
// 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
}

rawdb.WritePreimages(sdb.DiskDB(), statedb.Preimages())
sdb.TrieDB().WritePreimages()
snaps, err := snapshot.New(snapshot.Config{AsyncBuild: false, CacheSize: 10}, sdb.DiskDB(), sdb.TrieDB(), mptRoot)
Expand Down Expand Up @@ -522,6 +553,11 @@ func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prest
if err != nil {
panic(err)
}
} else {
statedb, err = state.New(mptRoot, sdb, 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.

In the case of a non-verkle (nor state-conversion) fork, we simply re-open the MPT with the mptRoot calculated before. This fixes consuming fixtures for those.

Copy link
Owner

Choose a reason for hiding this comment

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

I expect some pushback on the geth side, but we'll see. I'd prefer to be "agnostic" but they might prefer it to be obvious.

if err != nil {
panic(err)
}
}

if statedb.Database().InTransition() || statedb.Database().Transitioned() {
Expand Down
79 changes: 56 additions & 23 deletions cmd/evm/internal/t8ntool/transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,13 +548,65 @@ func VerkleKeys(ctx *cli.Context) error {
}
}

vkt, err := genVktFromAlloc(alloc)
if err != nil {
return fmt.Errorf("error generating vkt: %w", err)
}

collector := make(map[common.Hash]hexutil.Bytes)
it, err := vkt.NodeIterator(nil)
if err != nil {
panic(err)
}
for it.Next(true) {
if it.Leaf() {
collector[common.BytesToHash(it.LeafKey())] = it.LeafBlob()
}
}

output, err := json.MarshalIndent(collector, "", "")
if err != nil {
return fmt.Errorf("error outputting tree: %w", err)
}

fmt.Println(string(output))

return nil
}

// VerkleRoot computes the root of a VKT from a genesis alloc.
func VerkleRoot(ctx *cli.Context) error {
Comment on lines +577 to +578
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 is a new CLI tool to help the testing framework deal with a situation I found when fixing test filling.

The testing-library uses their own code for generating the genesis block. They only have an MPT implementation, so the genesis block was always generated with a MPT root. This is wrong for verkle-genesis tests.

Until they have a pure Python impl of a Verkle Tree and its cryptography, I offered them a new CLI that can calculate this root for them in the case we're in verkle mode. Whenever there's a execution-spec codebase implemented in Python for Verkle, they can copy-paste that implementation and come back generating the genesis vector on their side (and we can remove this CLI helper). For now, this should work and unblocked us from the problem.

var allocStr = ctx.String(InputAllocFlag.Name)
var alloc core.GenesisAlloc
if allocStr == stdinSelector {
decoder := json.NewDecoder(os.Stdin)
if err := decoder.Decode(&alloc); err != nil {
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling stdin: %v", err))
}
}
if allocStr != stdinSelector {
if err := readFile(allocStr, "alloc", &alloc); err != nil {
return err
}
}

vkt, err := genVktFromAlloc(alloc)
if err != nil {
return fmt.Errorf("error generating vkt: %w", err)
}
fmt.Println(vkt.Hash().Hex())

return nil
}

func genVktFromAlloc(alloc core.GenesisAlloc) (*trie.VerkleTrie, error) {
vkt := trie.NewVerkleTrie(verkle.New(), trie.NewDatabase(rawdb.NewMemoryDatabase()), utils.NewPointCache(), true)

for addr, acc := range alloc {
for slot, value := range acc.Storage {
err := vkt.UpdateStorage(addr, slot.Bytes(), value.Big().Bytes())
if err != nil {
return fmt.Errorf("error inserting storage: %w", err)
return nil, fmt.Errorf("error inserting storage: %w", err)
}
}

Expand All @@ -566,34 +618,15 @@ func VerkleKeys(ctx *cli.Context) error {
}
err := vkt.UpdateAccount(addr, account)
if err != nil {
return fmt.Errorf("error inserting account: %w", err)
return nil, fmt.Errorf("error inserting account: %w", err)
}

err = vkt.UpdateContractCode(addr, common.BytesToHash(account.CodeHash), acc.Code)
if err != nil {
return fmt.Errorf("error inserting code: %w", err)
}
}

collector := make(map[common.Hash]hexutil.Bytes)
it, err := vkt.NodeIterator(nil)
if err != nil {
panic(err)
}
for it.Next(true) {
if it.Leaf() {
collector[common.BytesToHash(it.LeafKey())] = it.LeafBlob()
return nil, fmt.Errorf("error inserting code: %w", err)
}
}

output, err := json.MarshalIndent(collector, "", "")
if err != nil {
return fmt.Errorf("error outputting tree: %w", err)
}

fmt.Println(string(output))

return nil
return vkt, nil
}

// VerkleCodeChunkKey computes the tree key of a code-chunk for a given address.
Expand Down
9 changes: 9 additions & 0 deletions cmd/evm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,15 @@ var verkleCommand = &cli.Command{
Usage: "chunkify a given bytecode",
Action: t8ntool.VerkleChunkifyCode,
},
{
Name: "state-root",
Aliases: []string{"VR"},
jsign marked this conversation as resolved.
Show resolved Hide resolved
Usage: "compute the state-root of a verkle tree for the given alloc",
Action: t8ntool.VerkleRoot,
Flags: []cli.Flag{
t8ntool.InputAllocFlag,
},
},
},
}

Expand Down
15 changes: 14 additions & 1 deletion core/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
Expand Down Expand Up @@ -172,6 +173,18 @@ func (ga *GenesisAlloc) flush(db ethdb.Database, triedb *trie.Database, blockhas
if err != nil {
return err
}

sdb := database
sdb.TrieDB().WritePreimages()
snaps, err := snapshot.New(snapshot.Config{AsyncBuild: false, CacheSize: 10}, sdb.DiskDB(), sdb.TrieDB(), types.EmptyRootHash)
if err != nil {
panic(err)
}
if snaps == nil {
panic("snapshot is nil")
}
snaps.Cap(types.EmptyRootHash, 0)
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 is a required fix to make state-conversion fixture consumption work. For that test (and future state-conversion ones), whenever we generate the genesis block before running test-blocks we need to enable pre-image recording.

This is needed since in the block execution of overlay tree we require the preimages of previous blocks (i.e: genesis block) to iterate the MPT and do the actual conversion.


// Commit newly generated states into disk if it's not empty.
if root != types.EmptyRootHash {
if err := triedb.Commit(root, true); err != nil {
Expand Down Expand Up @@ -560,7 +573,7 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *trie.Database) (*types.Block
// Note the state changes will be committed in hash-based scheme, use Commit
// if path-scheme is preferred.
func (g *Genesis) MustCommit(db ethdb.Database) *types.Block {
triedb := trie.NewDatabaseWithConfig(db, &trie.Config{Verkle: g.Config != nil && g.Config.IsVerkle(big.NewInt(int64(g.Number)), g.Timestamp)})
triedb := trie.NewDatabaseWithConfig(db, &trie.Config{Verkle: g.Config != nil && g.Config.IsVerkle(big.NewInt(int64(g.Number)), g.Timestamp), Preimages: true})
jsign marked this conversation as resolved.
Show resolved Hide resolved
block, err := g.Commit(db, triedb)
if err != nil {
panic(err)
Expand Down
4 changes: 3 additions & 1 deletion core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,9 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
// if the PC ends up in a new "chunk" of verkleized code, charge the
// associated costs.
contractAddr := contract.Address()
contract.Gas -= in.evm.TxContext.Accesses.TouchCodeChunksRangeAndChargeGas(contractAddr[:], pc, 1, uint64(len(contract.Code)), false)
if !contract.UseGas(in.evm.TxContext.Accesses.TouchCodeChunksRangeAndChargeGas(contractAddr[:], pc, 1, uint64(len(contract.Code)), false)) {
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 is a bug I found when filling the verkle-genesis tests. One of them made geth panic. After digging here was the cause.

The problem was that doing -= clearly can cause an integer underflow. This made geth crash later in the execution since the gas available was garbage.

The fix is simple, use the usual UseGas which does the required checks.

return nil, ErrOutOfGas
}
}

// Get the operation from the jump table and validate the stack to ensure there are
Expand Down
Loading
Loading