diff --git a/config/MONOREPO.md b/config/MONOREPO.md index f45d3e7cf5..ba5ed40794 100644 --- a/config/MONOREPO.md +++ b/config/MONOREPO.md @@ -97,4 +97,8 @@ Fixes code style according to the rules. Differently from `npm run lint`, this c Runs the package tests. +#### `npx vitest test//.spec.ts` + +Run & watch a single test file. Vitest will run the specified test file each time code in the associated package is updated so useful for prototyping when you are working on a specific issue/test case. + _Note that the VM has several test scopes - refer to [packages/vm/package.json](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/vm/package.json) for more info._ diff --git a/package-lock.json b/package-lock.json index 2c9bc6ed7e..13b3f5c7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2491,9 +2491,9 @@ ] }, "node_modules/@scure/base": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", - "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -2864,12 +2864,6 @@ "@types/node": "*" } }, - "node_modules/@types/jwt-simple": { - "version": "0.5.36", - "resolved": "https://registry.npmjs.org/@types/jwt-simple/-/jwt-simple-0.5.36.tgz", - "integrity": "sha512-UKOigRdpPdj7ODP+ouBC5U+7l0W0dAzGbrK23P0HFnTjVqbDAAEem8LIhTZf6NrDV/Bw8cxWQZnZ4DPBZkHfpA==", - "dev": true - }, "node_modules/@types/k-bucket": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/k-bucket/-/k-bucket-5.0.4.tgz", @@ -2939,6 +2933,30 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/@types/rollup-plugin-visualizer": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@types/rollup-plugin-visualizer/-/rollup-plugin-visualizer-4.2.4.tgz", + "integrity": "sha512-BW4Q6D1Qy5gno5qHWrnMDC2dOe/TAKXvqCpckOggCCu+XpS+ZZJJ1lq1+K3bvYccoO3Y7f5kglbFAgYGqCgULg==", + "dev": true, + "dependencies": { + "rollup": "^2.42.3" + } + }, + "node_modules/@types/rollup-plugin-visualizer/node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -3856,12 +3874,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4821,6 +4839,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -6199,9 +6226,9 @@ } }, "node_modules/ethers": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.11.1.tgz", - "integrity": "sha512-mxTAE6wqJQAbp5QAe/+o+rXOID7Nw91OZXvgpjDa1r4fAbq2Nu314oEZSbjoRLacuCzs7kUC3clEvkCQowffGg==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.1.tgz", + "integrity": "sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A==", "dev": true, "funding": [ { @@ -6220,7 +6247,7 @@ "@types/node": "18.15.13", "aes-js": "4.0.0-beta.5", "tslib": "2.4.0", - "ws": "8.5.0" + "ws": "8.17.1" }, "engines": { "node": ">=14.0.0" @@ -6262,27 +6289,6 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "dev": true }, - "node_modules/ethers/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -6446,9 +6452,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -7514,6 +7520,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7820,6 +7841,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -8062,9 +8095,9 @@ } }, "node_modules/jayson/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -8210,14 +8243,6 @@ "node": ">=4.0" } }, - "node_modules/jwt-simple": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/jwt-simple/-/jwt-simple-0.5.6.tgz", - "integrity": "sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8888,9 +8913,9 @@ } }, "node_modules/mcl-wasm": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-1.4.0.tgz", - "integrity": "sha512-90Tvmg2NXwnKMgTafA01PRELsYNNRb/F2bj3nzdByTLLMUmgkgL8H/oeWcjZtVVffnBJyNjDcYxY7cdOE/WoHg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-1.5.0.tgz", + "integrity": "sha512-+Bnefweg0PWhQ//pVAawNkZAC+TH/mMZVsxmEyHvw8Ujhwu3cxUe9WITFK74dfgPRB09Zkmf6aUFXnW23OnVUw==", "dependencies": { "@types/node": "^20.2.5" }, @@ -9777,6 +9802,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -10278,14 +10320,6 @@ "node": ">=6" } }, - "node_modules/qheap": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/qheap/-/qheap-1.4.0.tgz", - "integrity": "sha512-yb0qWRi8rOXCehqmxxp7gM/x/5GqYYpRsvZ9wvbcOSVD0nrmi6BIVO+DpGstSDsDPYbW54lppA7GoNeMpv6q0w==", - "engines": { - "node": "*" - } - }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -10633,6 +10667,111 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "dev": true, + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12154,9 +12293,9 @@ } }, "node_modules/verkle-cryptography-wasm": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/verkle-cryptography-wasm/-/verkle-cryptography-wasm-0.4.2.tgz", - "integrity": "sha512-Mji9ibiCBS0ObveKuIC6XY009zLJTVBwwPaLnOI3yoj26MbZjAI8ByhWSmoQCJkWj/j/hNjcq9Jbx39Fguh92w==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/verkle-cryptography-wasm/-/verkle-cryptography-wasm-0.4.5.tgz", + "integrity": "sha512-CG0hRG0QuVoLnmwPiBpgWWzgJCoF81/w6S3ASeoew6tQp52HsN+e8yrachGh2bFIZLRDUQA2cX+0xr2VM60ywA==", "dependencies": { "@scure/base": "^1.1.5" }, @@ -13098,9 +13237,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -13284,6 +13423,7 @@ "@multiformats/multiaddr": "^12.2.1", "@polkadot/util": "^12.6.2", "@polkadot/wasm-crypto": "^7.3.2", + "@scure/base": "^1.1.7", "abstract-level": "^1.0.3", "body-parser": "^1.19.2", "chalk": "^4.1.2", @@ -13294,12 +13434,11 @@ "it-pipe": "^1.1.0", "jayson": "^4.0.0", "js-sdsl": "^4.4.0", - "jwt-simple": "^0.5.6", "kzg-wasm": "^0.4.0", "level": "^8.0.0", "memory-level": "^1.0.0", "prom-client": "^15.1.0", - "qheap": "^1.4.0", + "verkle-cryptography-wasm": "^0.4.5", "winston": "^3.3.3", "winston-daily-rotate-file": "^4.5.5", "yargs": "^17.7.1" @@ -13312,7 +13451,6 @@ "@types/connect": "^3.4.35", "@types/cors": "^2.8.17", "@types/fs-extra": "^11.0.4", - "@types/jwt-simple": "^0.5.33", "@types/ws": "^8.5.10", "@types/yargs": "^17.0.24", "eventsource": "^2.0.2", @@ -13411,7 +13549,7 @@ "@ethereumjs/common": "^4.3.0", "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/util": "^9.0.3", - "@scure/base": "1.1.1", + "@scure/base": "^1.1.7", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", "lru-cache": "10.1.0", @@ -13430,17 +13568,6 @@ "node": ">=18" } }, - "packages/devp2p/node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, "packages/ethash": { "name": "@ethereumjs/ethash", "version": "3.0.3", @@ -13468,11 +13595,10 @@ "@ethereumjs/statemanager": "^2.3.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", - "@ethereumjs/verkle": "^0.0.2", "@types/debug": "^4.1.9", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", - "mcl-wasm": "^1.4.0", + "mcl-wasm": "^1.5.0", "rustbn-wasm": "^0.4.0" }, "devDependencies": { @@ -13481,12 +13607,14 @@ "@types/core-js": "^2.5.0", "@types/minimist": "^1.2.2", "@types/node-dir": "^0.0.34", + "@types/rollup-plugin-visualizer": "^4.2.4", "benchmark": "^2.1.4", "kzg-wasm": "^0.4.0", "level": "^8.0.0", "memory-level": "^1.0.0", "minimist": "^1.2.5", "node-dir": "^0.1.17", + "rollup-plugin-visualizer": "^5.12.0", "solc": "^0.8.1" }, "engines": { @@ -13531,18 +13659,17 @@ "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/trie": "^6.2.0", "@ethereumjs/util": "^9.0.3", - "@ethereumjs/verkle": "^0.0.2", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", "js-sdsl": "^4.1.4", - "lru-cache": "10.1.0", - "verkle-cryptography-wasm": "^0.4.2" + "lru-cache": "10.1.0" }, "devDependencies": { "@ethereumjs/block": "^5.2.0", "@ethereumjs/genesis": "^0.2.2", "@types/debug": "^4.1.9", - "rustbn-wasm": "^0.4.0" + "rustbn-wasm": "^0.4.0", + "verkle-cryptography-wasm": "^0.4.5" } }, "packages/trie": { @@ -13618,8 +13745,9 @@ "@ethereumjs/block": "^5.2.0", "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/util": "^9.0.3", + "debug": "^4.3.4", "lru-cache": "10.1.0", - "verkle-cryptography-wasm": "^0.4.2" + "verkle-cryptography-wasm": "^0.4.5" }, "engines": { "node": ">=18" @@ -13639,10 +13767,9 @@ "@ethereumjs/trie": "^6.2.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", - "@ethereumjs/verkle": "^0.0.2", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", - "mcl-wasm": "^1.4.0" + "mcl-wasm": "^1.5.0" }, "devDependencies": { "@ethersproject/abi": "^5.0.12", @@ -13667,7 +13794,7 @@ "license": "MIT", "dependencies": { "@ethereumjs/util": "^9.0.3", - "@scure/base": "^1.1.5", + "@scure/base": "^1.1.7", "ethereum-cryptography": "^2.1.3", "js-md5": "^0.8.3", "uuid": "^9.0.1" diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 3b914193a3..2670b096cd 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -38,7 +38,6 @@ import type { JsonBlock, JsonRpcBlock, RequestsBytes, - VerkleExecutionWitness, WithdrawalsBytes, } from './types.js' import type { Common } from '@ethereumjs/common' @@ -53,6 +52,7 @@ import type { EthersProvider, PrefixedHexString, RequestBytes, + VerkleExecutionWitness, WithdrawalBytes, } from '@ethereumjs/util' diff --git a/packages/block/src/from-beacon-payload.ts b/packages/block/src/from-beacon-payload.ts index 5de5da9f44..3ad5257a44 100644 --- a/packages/block/src/from-beacon-payload.ts +++ b/packages/block/src/from-beacon-payload.ts @@ -1,7 +1,7 @@ import { bigIntToHex } from '@ethereumjs/util' -import type { ExecutionPayload, VerkleExecutionWitness } from './types.js' -import type { PrefixedHexString } from '@ethereumjs/util' +import type { ExecutionPayload } from './types.js' +import type { PrefixedHexString, VerkleExecutionWitness } from '@ethereumjs/util' type BeaconWithdrawal = { index: PrefixedHexString @@ -20,7 +20,7 @@ type BeaconDepositRequest = { type BeaconWithdrawalRequest = { source_address: PrefixedHexString - validator_public_key: PrefixedHexString + validator_pub_key: PrefixedHexString amount: PrefixedHexString } @@ -160,7 +160,7 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJson): E if (payload.withdrawal_requests !== undefined && payload.withdrawal_requests !== null) { executionPayload.withdrawalRequests = payload.withdrawal_requests.map((breq) => ({ sourceAddress: breq.source_address, - validatorPublicKey: breq.validator_public_key, + validatorPubkey: breq.validator_pub_key, amount: breq.amount, })) } diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 332911d0af..8c08048dc4 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -11,6 +11,7 @@ import type { JsonRpcWithdrawal, PrefixedHexString, RequestBytes, + VerkleExecutionWitness, WithdrawalBytes, WithdrawalData, WithdrawalRequestV1, @@ -78,45 +79,6 @@ export interface BlockOptions { executionWitness?: VerkleExecutionWitness } -export interface VerkleProof { - commitmentsByPath: PrefixedHexString[] - d: PrefixedHexString - depthExtensionPresent: PrefixedHexString - ipaProof: { - cl: PrefixedHexString[] - cr: PrefixedHexString[] - finalEvaluation: PrefixedHexString - } - otherStems: PrefixedHexString[] -} - -export interface VerkleStateDiff { - stem: PrefixedHexString - suffixDiffs: { - currentValue: PrefixedHexString | null - newValue: PrefixedHexString | null - suffix: number | string - }[] -} - -/** - * Experimental, object format could eventual change. - * An object that provides the state and proof necessary for verkle stateless execution - * */ -export interface VerkleExecutionWitness { - /** - * An array of state diffs. - * Each item corresponding to state accesses or state modifications of the block. - * In the current design, it also contains the resulting state of the block execution (post-state). - */ - stateDiff: VerkleStateDiff[] - /** - * The verkle proof for the block. - * Proves that the provided stateDiff belongs to the canonical verkle tree. - */ - verkleProof: VerkleProof -} - /** * A block header's data. */ diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index 1d1962aacb..7c1c2f293a 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -26,7 +26,7 @@ function getRandomDepositRequest(): CLRequest { function getRandomWithdrawalRequest(): CLRequest { const withdrawalRequestData = { sourceAddress: randomBytes(20), - validatorPublicKey: randomBytes(48), + validatorPubkey: randomBytes(48), amount: bytesToBigInt(randomBytes(8)), } return WithdrawalRequest.fromRequestData(withdrawalRequestData) as CLRequest diff --git a/packages/block/test/from-beacon-payload.spec.ts b/packages/block/test/from-beacon-payload.spec.ts index d8e0a5fc05..0faf8f31d0 100644 --- a/packages/block/test/from-beacon-payload.spec.ts +++ b/packages/block/test/from-beacon-payload.spec.ts @@ -10,7 +10,8 @@ import * as payload87335 from './testdata/payload-slot-87335.json' import * as payload87475 from './testdata/payload-slot-87475.json' import * as testnetVerkleKaustinen from './testdata/testnetVerkleKaustinen.json' -import type { BeaconPayloadJson, VerkleExecutionWitness } from '../src/index.js' +import type { BeaconPayloadJson } from '../src/index.js' +import type { VerkleExecutionWitness } from '@ethereumjs/util' describe('[fromExecutionPayloadJson]: 4844 devnet 5', () => { let common: Common diff --git a/packages/client/archive/libp2p/net/package.json.browser.deps b/packages/client/archive/libp2p/net/package.json.browser.deps index 286c43f04c..64d8b0ca98 100644 --- a/packages/client/archive/libp2p/net/package.json.browser.deps +++ b/packages/client/archive/libp2p/net/package.json.browser.deps @@ -80,7 +80,6 @@ "fs-extra": "^10.1.0", "it-pipe": "^1.1.0", "jayson": "^4.0.0", - "jwt-simple": "^0.5.6", "level": "^8.0.0", "memory-level": "^1.0.0", "peer-id": "^0.14.3", @@ -133,7 +132,6 @@ "@types/body-parser": "^1.19.2", "@types/connect": "^3.4.35", "@types/fs-extra": "^9.0.13", - "@types/jwt-simple": "^0.5.33", "@types/yargs": "^17.0.24", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.12.0", diff --git a/packages/client/bin/cli.ts b/packages/client/bin/cli.ts index aacff167b8..e305d87fd8 100755 --- a/packages/client/bin/cli.ts +++ b/packages/client/bin/cli.ts @@ -422,6 +422,7 @@ const args: ClientOpts = yargs .option('loadBlocksFromRlp', { describe: 'path to a file of RLP encoded blocks', string: true, + array: true, }) .option('pruneEngineCache', { describe: 'Enable/Disable pruning engine block cache (disable for testing against hive etc)', @@ -652,27 +653,29 @@ async function startClient( if (args.loadBlocksFromRlp !== undefined) { // Specifically for Hive simulator, preload blocks provided in RLP format - const blockRlp = readFileSync(args.loadBlocksFromRlp) const blocks: Block[] = [] - let buf = RLP.decode(blockRlp, true) - while (buf.data?.length > 0 || buf.remainder?.length > 0) { - try { - const block = Block.fromValuesArray(buf.data as BlockBytes, { - common: config.chainCommon, - setHardfork: true, - }) - blocks.push(block) - buf = RLP.decode(buf.remainder, true) - config.logger.info( - `Preloading block hash=0x${short(bytesToHex(block.header.hash()))} number=${ - block.header.number - }` - ) - } catch (err: any) { - config.logger.info( - `Encountered error while while preloading chain data error=${err.message}` - ) - break + for (const rlpBlock of args.loadBlocksFromRlp) { + const blockRlp = readFileSync(rlpBlock) + let buf = RLP.decode(blockRlp, true) + while (buf.data?.length > 0 || buf.remainder?.length > 0) { + try { + const block = Block.fromValuesArray(buf.data as BlockBytes, { + common: config.chainCommon, + setHardfork: true, + }) + blocks.push(block) + buf = RLP.decode(buf.remainder, true) + config.logger.info( + `Preloading block hash=0x${short(bytesToHex(block.header.hash()))} number=${ + block.header.number + }` + ) + } catch (err: any) { + config.logger.info( + `Encountered error while while preloading chain data error=${err.message}` + ) + break + } } } @@ -681,7 +684,7 @@ async function startClient( await client.chain.open() } - await client.chain.putBlocks(blocks) + await client.chain.putBlocks(blocks, true) } } diff --git a/packages/client/package.json b/packages/client/package.json index a5b59ec3cd..5dae753eed 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -72,6 +72,7 @@ "@multiformats/multiaddr": "^12.2.1", "@polkadot/util": "^12.6.2", "@polkadot/wasm-crypto": "^7.3.2", + "@scure/base": "^1.1.7", "abstract-level": "^1.0.3", "body-parser": "^1.19.2", "chalk": "^4.1.2", @@ -82,12 +83,11 @@ "it-pipe": "^1.1.0", "jayson": "^4.0.0", "js-sdsl": "^4.4.0", - "jwt-simple": "^0.5.6", "kzg-wasm": "^0.4.0", "level": "^8.0.0", "memory-level": "^1.0.0", "prom-client": "^15.1.0", - "qheap": "^1.4.0", + "verkle-cryptography-wasm": "^0.4.5", "winston": "^3.3.3", "winston-daily-rotate-file": "^4.5.5", "yargs": "^17.7.1" @@ -97,7 +97,6 @@ "@types/connect": "^3.4.35", "@types/cors": "^2.8.17", "@types/fs-extra": "^11.0.4", - "@types/jwt-simple": "^0.5.33", "@types/ws": "^8.5.10", "@types/yargs": "^17.0.24", "eventsource": "^2.0.2", diff --git a/packages/client/src/execution/vmexecution.ts b/packages/client/src/execution/vmexecution.ts index 2aad9d5ebf..83ee15bab7 100644 --- a/packages/client/src/execution/vmexecution.ts +++ b/packages/client/src/execution/vmexecution.ts @@ -23,6 +23,7 @@ import { } from '@ethereumjs/util' import { VM } from '@ethereumjs/vm' import { writeFileSync } from 'fs' +import { loadVerkleCrypto } from 'verkle-cryptography-wasm' import { Event } from '../types.js' import { debugCodeReplayBlock } from '../util/debug.js' @@ -190,8 +191,10 @@ export class VMExecution extends Execution { return } this.config.logger.info(`Setting up verkleVM`) - const stateManager = await StatelessVerkleStateManager.create({ + const verkleCrypto = await loadVerkleCrypto() + const stateManager = new StatelessVerkleStateManager({ initialStateRoot: this.config.initialVerkleStateRoot, + verkleCrypto, }) this.verkleVM = await VM.create({ common: this.config.execCommon, diff --git a/packages/client/src/ext/index.ts b/packages/client/src/ext/index.ts new file mode 100644 index 0000000000..f123349ad4 --- /dev/null +++ b/packages/client/src/ext/index.ts @@ -0,0 +1,3 @@ +'use strict' + +export * from './qheap.js' diff --git a/packages/client/src/ext/jwt-simple.ts b/packages/client/src/ext/jwt-simple.ts new file mode 100644 index 0000000000..9357ef52c3 --- /dev/null +++ b/packages/client/src/ext/jwt-simple.ts @@ -0,0 +1,202 @@ +/** + * Ported to Typescript from original implementation below: + * https://github.com/hokaccha/node-jwt-simple -- MIT licensed + */ + +/** + * module dependencies + */ +import { bytesToUtf8, utf8ToBytes } from '@ethereumjs/util' +import { base64url } from '@scure/base' +import crypto from 'crypto' + +/** + * support algorithm mapping + */ +export type TAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' +const algorithmMap: Record = { + HS256: 'sha256', + HS384: 'sha384', + HS512: 'sha512', + RS256: 'RSA-SHA256', +} + +/** + * Map algorithm to hmac or sign type, to determine which crypto function to use + */ +const typeMap: Record = { + HS256: 'hmac', + HS384: 'hmac', + HS512: 'hmac', + RS256: 'sign', +} + +/** + * expose object + */ + +/** + * private util functions + */ + +function assignProperties(dest: any, source: any) { + for (const [attr] of Object.entries(source)) { + if (Object.prototype.hasOwnProperty.call(source, attr)) { + dest[attr] = source[attr] + } + } +} + +function assertAlgorithm(alg: any): asserts alg is Algorithm { + if (!['HS256', 'HS384', 'HS512', 'RS256'].includes(alg)) { + throw new Error('Algorithm not supported') + } +} + +function base64urlUnescape(str: string) { + str += new Array(5 - (str.length % 4)).join('=') + return str.replace(/-/g, '+').replace(/_/g, '/') +} + +function base64urlEscape(str: string) { + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} + +function sign(input: any, key: string, method: string, type: string) { + let base64str + if (type === 'hmac') { + base64str = crypto.createHmac(method, key).update(input).digest('base64') + } else if (type === 'sign') { + base64str = crypto.createSign(method).update(input).sign(key, 'base64') + } else { + throw new Error('Algorithm type not recognized') + } + + return base64urlEscape(base64str) +} + +function verify(input: any, key: string, method: string, type: string, signature: string) { + if (type === 'hmac') { + return signature === sign(input, key, method, type) + } else if (type === 'sign') { + return crypto + .createVerify(method) + .update(input) + .verify(key, base64urlUnescape(signature), 'base64') + } else { + throw new Error('Algorithm type not recognized') + } +} + +/** + * Decode jwt + * + * @param {Object} token + * @param {String} key + * @param {Boolean} [noVerify] + * @param {String} [algorithm] + * @return {Object} payload + * @api public + */ +const decode = function jwt_decode( + token: string, + key: string, + noVerify: boolean = false, + algorithm: string = '' +) { + // check token + if (!token) { + throw new Error('No token supplied') + } + // check segments + const segments = token.split('.') + if (segments.length !== 3) { + throw new Error('Not enough or too many segments') + } + + // All segment should be base64 + const headerSeg = segments[0] + const payloadSeg = segments[1] + const signatureSeg = segments[2] + + // base64 decode and parse JSON + const header = JSON.parse(bytesToUtf8(base64url.decode(headerSeg))) + const payload = JSON.parse(bytesToUtf8(base64url.decode(payloadSeg))) + + if (!noVerify) { + if (!algorithm && /BEGIN( RSA)? PUBLIC KEY/.test(key.toString())) { + algorithm = 'RS256' + } + + algorithm = algorithm || header.alg + + assertAlgorithm(algorithm) + const signingMethod = algorithmMap[algorithm] + const signingType = typeMap[algorithm] + + // verify signature. `sign` will return base64 string. + const signingInput = [headerSeg, payloadSeg].join('.') + if (verify(signingInput, key, signingMethod, signingType, signatureSeg) === false) { + throw new Error('Signature verification failed') + } + + // Support for nbf and exp claims. + // According to the RFC, they should be in seconds. + if (payload.nbf !== undefined && Date.now() < payload.nbf * 1000) { + throw new Error('Token not yet active') + } + + if (payload.exp !== undefined && Date.now() > payload.exp * 1000) { + throw new Error('Token expired') + } + } + + return payload +} + +/** + * Encode jwt + * + * @param {Object} payload + * @param {String} key + * @param {String} algorithm + * @param {Object} options + * @return {String} token + * @api public + */ +const encode = function jwt_encode( + payload: any, + key: string, + algorithm: string = '', + options: any = undefined +) { + // Check key + if (!key) { + throw new Error('Require key') + } + + // Check algorithm, default is HS256 + if (!algorithm) { + algorithm = 'HS256' + } + + assertAlgorithm(algorithm) + const signingMethod = algorithmMap[algorithm] + const signingType = typeMap[algorithm] + + // header, typ is fixed value. + const header = { typ: 'JWT', alg: algorithm } + if (options !== undefined && options.header !== undefined) { + assignProperties(header, options.header) + } + + // create segments, all segments should be base64 string + const segments = [] + segments.push(base64url.encode(utf8ToBytes(JSON.stringify(header)))) + segments.push(base64url.encode(utf8ToBytes(JSON.stringify(payload)))) + segments.push(sign(segments.join('.'), key, signingMethod, signingType)) + + return segments.join('.') +} + +export const jwt = { encode, decode } diff --git a/packages/client/src/ext/qheap.ts b/packages/client/src/ext/qheap.ts new file mode 100644 index 0000000000..346245b232 --- /dev/null +++ b/packages/client/src/ext/qheap.ts @@ -0,0 +1,216 @@ +/** + * nodejs heap, classic array implementation + * + * Items are stored in a balanced binary tree packed into an array where + * node is at [i], left child is at [2*i], right at [2*i+1]. Root is at [1]. + * + * Copyright (C) 2014-2021 Andras Radics + * Licensed under the Apache License, Version 2.0 + */ + +/** + * QHeap types. + * @types/qheap does not exist, so we define it here. + * https://www.npmjs.com/package/qheap + */ +export type QHeapOptions = { + comparBefore?(a: any, b: any): boolean + compar?(a: any, b: any): number + freeSpace?: number + size?: number +} +export type QHeap = { + // constructor(opts?: QHeapOptions) + insert(item: T): void + push(item: T): void + enqueue(item: T): void + remove(): T | undefined + shift(): T | undefined + dequeue(): T | undefined + peek(): T | undefined + length: number + gc(opts: { minLength: number; maxLength: number }): void +} + +export class Heap { + private _list!: any[] + private _isBefore!: (a: any, b: any) => boolean + private _sortBefore!: (a: any, b: any) => number + private _freeSpace!: ((list: any[], len: number) => void) | false + public options!: QHeapOptions + public length!: number + + constructor(opts?: QHeapOptions | Function) { + if (!(this instanceof Heap)) return new Heap(opts as QHeapOptions) + + if (typeof opts === 'function') opts = { compar: opts as any } + + // copy out known options to not bind to caller object + this.options = !opts + ? ({} as QHeapOptions) + : { + compar: (opts as QHeapOptions).compar, + comparBefore: (opts as QHeapOptions).comparBefore, + freeSpace: (opts as QHeapOptions).freeSpace, + size: (opts as QHeapOptions).size, + } + opts = this.options + + const self = this + + this._isBefore = opts.compar + ? function (a: any, b: any) { + // @ts-ignore + return opts!.compar!(a, b) < 0 + } + : opts.comparBefore ?? + function (a: any, b: any): boolean { + return a < b + } + + this._sortBefore = + opts.compar ?? + function (a: any, b: any) { + return self._isBefore(a, b) ? -1 : 1 + } + this._freeSpace = opts.freeSpace === undefined ? this._trimArraySize : false + + this._list = new Array(opts.size ?? 20) + this.length = 0 + } + + /* + * insert new item at end, and bubble up + */ + public insert(item: any): any { + const idx = ++this.length + return this._bubbleup(idx, item) + } + public _bubbleup(idx: number, item: any): void { + const list = this._list + list[idx] = item + if (idx <= 1) return + do { + const pp = idx >>> 1 + if (this._isBefore(item, list[pp])) list[idx] = list[pp] + else break + idx = pp + } while (idx > 1) + list[idx] = item + } + public append = this.insert + public push = this.insert + public unshift = this.insert + public enqueue = this.insert + + public peek(): any { + return this.length > 0 ? this._list[1] : undefined + } + + public size(): number { + return this.length + } + + /* + * return the root, and bubble down last item from top root position + * when bubbling down, r: root idx, c: child sub-tree root idx, cv: child root value + * Note that the child at (c == this.length) does not have to be tested in the loop, + * since its value is the one being bubbled down, so can loop `while (c < len)`. + */ + public remove(): any { + const len = this.length + if (len < 1) return undefined + return this._bubbledown(1, len) + } + public _bubbledown(r: number, len: number): any { + const list = this._list, + ret = list[r], + itm = list[len] + let c + const _isBefore = this._isBefore + + while ((c = r << 1) < len) { + let cv = list[c] + const cv1 = list[c + 1] + if (_isBefore(cv1, cv)) { + c++ + cv = cv1 + } + if (!_isBefore(cv, itm)) break + list[r] = cv + r = c + } + list[r] = itm + list[len] = 0 + this.length = --len + if (this._freeSpace !== false && this._freeSpace !== undefined) + this._freeSpace(this._list, this.length) + + return ret + } + + public shift = this.remove + public pop = this.remove + public dequeue = this.remove + + // builder, not initializer: appends items, not replaces + // FIXME: more useful to re-initialize from array + public fromArray(array: any[], base?: number, bound?: number): void { + base = (base ?? 0) || 0 + bound = (bound ?? 0) || array.length + for (let i = base; i < bound; i++) this.insert(array[i]) + } + + // FIXME: more useful to return sorted values + public toArray(limit?: number): any[] { + limit = typeof limit === 'number' ? limit + 1 : this.length + 1 + return this._list.slice(1, limit) + } + + // sort the contents of the storage array + public sort(): void { + if (this.length < 3) return + this._list.splice(this.length + 1) + this._list[0] = this._list[1] + this._list.sort(this._sortBefore) + this._list[0] = 0 + } + + // Free unused storage slots in the _list. + public gc(options?: { minLength?: number; minFull?: number }): void { + if (!options) options = {} + + const minListLength = (options.minLength ?? 0) || 0 + const minListFull = (options.minFull ?? 0) || 1.0 + + if (this._list.length >= minListLength && this.length < this._list.length * minListFull) { + this._list.splice(this.length + 1, this._list.length) + } + } + + public _trimArraySize(list: any[], len: number): void { + if (len > 10000 && list.length > 4 * len) { + list.splice(len + 1, list.length) + } + } + + public _check(): boolean { + const _compar = this._sortBefore + + let i, + p, + fail = 0 + for (i = this.length; i > 1; i--) { + // error if parent should go after child, but not if don`t care + p = i >>> 1 + // swapping the values must change their ordering, otherwise the + // comparison is a tie. (Ie, consider the ordering func (a <= b) + // that for some values reports both that a < b and b < a.) + if (_compar(this._list[p], this._list[i]) > 0 && _compar(this._list[i], this._list[p]) < 0) { + fail = i + } + } + if (fail) console.log('failed at', fail >>> 1, fail) + return !fail + } +} diff --git a/packages/client/src/rpc/validation.ts b/packages/client/src/rpc/validation.ts index d6207289fd..84afefdb48 100644 --- a/packages/client/src/rpc/validation.ts +++ b/packages/client/src/rpc/validation.ts @@ -485,7 +485,7 @@ export const validators = { }, get withdrawalRequest() { - return (requiredFields: string[] = ['sourceAddress', 'validatorPublicKey', 'amount']) => { + return (requiredFields: string[] = ['sourceAddress', 'validatorPubkey', 'amount']) => { return (params: any[], index: number) => { if (typeof params[index] !== 'object') { return { @@ -517,8 +517,8 @@ export const validators = { if (v !== undefined) return v } - // validate validatorPublicKey - for (const field of [wt.validatorPublicKey]) { + // validate validatorPubkey + for (const field of [wt.validatorPubkey]) { const v = validate(field, this.bytes48) if (v !== undefined) return v } diff --git a/packages/client/src/service/txpool.ts b/packages/client/src/service/txpool.ts index 51a66f33c3..693b5e9231 100644 --- a/packages/client/src/service/txpool.ts +++ b/packages/client/src/service/txpool.ts @@ -16,9 +16,11 @@ import { equalsBytes, hexToBytes, } from '@ethereumjs/util' -import Heap from 'qheap' + +import { Heap } from '../ext/qheap.js' import type { Config } from '../config.js' +import type { QHeap } from '../ext/qheap.js' import type { Peer } from '../net/peer/peer.js' import type { PeerPool } from '../net/peerpool.js' import type { FullEthereumService } from './fullethereumservice.js' @@ -29,7 +31,6 @@ import type { TypedTransaction, } from '@ethereumjs/tx' import type { VM } from '@ethereumjs/vm' -import type QHeap from 'qheap' // Configuration constants const MIN_GAS_PRICE_BUMP_PERCENT = 10 diff --git a/packages/client/src/sync/fetcher/fetcher.ts b/packages/client/src/sync/fetcher/fetcher.ts index 9a211e7187..c2750bf917 100644 --- a/packages/client/src/sync/fetcher/fetcher.ts +++ b/packages/client/src/sync/fetcher/fetcher.ts @@ -1,16 +1,16 @@ import debugDefault from 'debug' -import Heap from 'qheap' import { Readable, Writable } from 'stream' +import { Heap } from '../../ext/qheap.js' import { Event } from '../../types.js' import type { Config } from '../../config.js' +import type { QHeap } from '../../ext/qheap.js' import type { Peer } from '../../net/peer/index.js' import type { PeerPool } from '../../net/peerpool.js' import type { JobTask as BlockFetcherJobTask } from './blockfetcherbase.js' import type { Job } from './types.js' import type { Debugger } from 'debug' -import type QHeap from 'qheap' const { debug: createDebugLogger } = debugDefault diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 91b25a6ea5..bbebaabb05 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -166,7 +166,7 @@ export interface ClientOpts { isSingleNode?: boolean vmProfileBlocks?: boolean vmProfileTxs?: boolean - loadBlocksFromRlp?: string + loadBlocksFromRlp?: string[] pruneEngineCache?: boolean savePreimages?: boolean verkleGenesisStateRoot?: Uint8Array diff --git a/packages/client/src/util/rpc.ts b/packages/client/src/util/rpc.ts index e040563f6c..9eb48cfc89 100644 --- a/packages/client/src/util/rpc.ts +++ b/packages/client/src/util/rpc.ts @@ -3,14 +3,15 @@ import Connect from 'connect' import cors from 'cors' import { createServer } from 'http' import jayson from 'jayson/promise/index.js' -import jwt from 'jwt-simple' import { inspect } from 'util' +import { jwt } from '../ext/jwt-simple.js' + +import type { TAlgorithm } from '../ext/jwt-simple.js' import type { Logger } from '../logging.js' import type { RPCManager } from '../rpc/index.js' import type { IncomingMessage } from 'connect' import type { HttpServer } from 'jayson/promise' -import type { TAlgorithm } from 'jwt-simple' const { json: jsonParser } = bodyParser const { decode } = jwt diff --git a/packages/client/test/ext/jwt-simple.spec.ts b/packages/client/test/ext/jwt-simple.spec.ts new file mode 100644 index 0000000000..3fa9197a2f --- /dev/null +++ b/packages/client/test/ext/jwt-simple.spec.ts @@ -0,0 +1,131 @@ +import { bytesToUtf8 } from '@ethereumjs/util' +import { base64url } from '@scure/base' +import fs from 'fs' +import { describe, expect, it } from 'vitest' + +import { jwt } from '../../src/ext/jwt-simple.js' + +describe('jwt', function () { + it('jwt has `encode` method', function () { + expect(jwt.encode).to.be.a('function') + }) + + it('jwt has `decode` method', function () { + expect(jwt.decode).to.be.a('function') + }) +}) + +describe('encode', function () { + it('encode token', function () { + const token = jwt.encode({ foo: 'bar' }, 'key') + expect(token).to.be.a('string') + expect(token.split('.')).to.have.length(3) + }) + + it('throw an error when the key is missing', function () { + const fn = jwt.encode.bind(null, { foo: 'bar' }) + expect(fn).toThrowError(/Require key/) + }) + + it('throw an error when the specified algorithm is not supported', function () { + const fn = jwt.encode.bind(null, { foo: 'bar' }, 'some_key', 'FooBar256') + expect(fn).toThrowError(/Algorithm not supported/) + }) +}) + +describe('decode', function () { + const key = 'key' + const obj = { foo: 'bar' } + const token = jwt.encode(obj, key) + + it('decode token', function () { + const obj2 = jwt.decode(token, key) + expect(obj2).to.eql(obj) + }) + + it('throw an error when no token is provided', function () { + const fn = jwt.decode.bind(null, null, key) + expect(fn).toThrowError(/No token supplied/) + }) + + it('throw an error when the token is not correctly formatted', function () { + const fn = jwt.decode.bind(null, 'foo.bar', key) + expect(fn).toThrowError(/Not enough or too many segments/) + }) + + it('throw an error when the specified algorithm is not supported', function () { + const fn = jwt.decode.bind(null, token, key, false, 'FooBar256') + expect(fn).toThrowError(/Algorithm not supported/) + }) + + it('throw an error when the signature verification fails', function () { + const fn = jwt.decode.bind(null, token, 'invalid_key') + expect(fn).toThrowError(/Signature verification failed/) + }) + + it('throw an error when the token is not yet active (optional nbf claim)', function () { + const nbf = (Date.now() + 1000) / 1000 + const token = jwt.encode({ foo: 'bar', nbf }, key) + const fn = jwt.decode.bind(null, token, key) + expect(fn).toThrowError(/Token not yet active/) + }) + + it('throw an error when the token has expired (optional exp claim)', function () { + const exp = (Date.now() - 1000) / 1000 + const token = jwt.encode({ foo: 'bar', exp }, key) + const fn = jwt.decode.bind(null, token, key) + expect(fn).toThrowError(/Token expired/) + }) + + it('do not throw any error when verification is disabled', function () { + const obj = { foo: 'bar' } + const key = 'key' + const token = jwt.encode(obj, key) + const fn1 = jwt.decode.bind(null, token, 'invalid_key1') + const fn2 = jwt.decode.bind(null, token, 'invalid_key2', true) + expect(fn1).toThrowError(/Signature verification failed/) + expect(fn2()).to.eql(obj) + }) + + it('decode token given algorithm', function () { + const obj = { foo: 'bar' } + const key = 'key' + const token = jwt.encode(obj, key, 'HS512') + const obj2 = jwt.decode(token, key, false, 'HS512') + expect(obj2).to.eql(obj) + expect(jwt.decode.bind(null, token, key, false, 'HS256')).toThrowError( + /Signature verification failed/ + ) + }) + + describe('RS256', function () { + const obj = { foo: 'bar' } + const pem = fs.readFileSync(__dirname + '/test.pem').toString('ascii') + const cert = fs.readFileSync(__dirname + '/test.crt').toString('ascii') + const alg = 'RS256' + + it('can add jwt header by options.header', function () { + const token = jwt.encode(obj, pem, alg, { header: { kid: 'keyidX' } }) + const obj2 = jwt.decode(token, cert) + expect(obj2).to.eql(obj) + + const jwtHeader = token.split('.')[0] + expect(JSON.parse(bytesToUtf8(base64url.decode(jwtHeader)))).to.eql({ + typ: 'JWT', + alg, + kid: 'keyidX', + }) + }) + + it('decode token given RS256 algorithm', function () { + const token = jwt.encode(obj, pem, alg) + const obj2 = jwt.decode(token, cert) + expect(obj2).to.eql(obj) + }) + + it('throw an error when the key is invalid', function () { + const token = jwt.encode(obj, pem, alg) + expect(jwt.decode.bind(null, token, 'invalid_key')).toThrowError() + }) + }) +}) diff --git a/packages/client/test/ext/test.crt b/packages/client/test/ext/test.crt new file mode 100644 index 0000000000..e933ff0cfe --- /dev/null +++ b/packages/client/test/ext/test.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICATCCAWoCCQDoLzF89AVR9jANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB +VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTE0MDMxODA5MzgzOVoXDTE1MDMxODA5MzgzOVowRTELMAkG +A1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxkf+ +aQuof/FiI1ejRl/385JhGbOq9ZUD0Ma7FELpkW+Wb9k3dxFRXjIeZgbMr5kUtzGv +jMA+IJpMPmqHOLMUG731xxmXoHphlhWKV1TTR8OXduIxRB+frVhYfp0nOAZroO+5 +sXBrGwCcFFjsDBhLLf7R1d9WdkS/LQ0rBi7GvaMCAwEAATANBgkqhkiG9w0BAQUF +AAOBgQCNxfthoxLOFZidvviG6aFjFgY35eFqv3RLHWAVBWQBHfjczph/r5mlT06z +AOKO7yp23Gi2dyBYaeq1u6n7iyMp9htYee8Y+Erlp6vurvi9S+/8mNVAPBtQ1kNw +KvzMTvylD2zWjopwMb9bfSKKT5pe7pZ7CS6Y5T8lM9yZlMBhHQ== +-----END CERTIFICATE----- diff --git a/packages/client/test/ext/test.pem b/packages/client/test/ext/test.pem new file mode 100644 index 0000000000..3c622ea41c --- /dev/null +++ b/packages/client/test/ext/test.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDGR/5pC6h/8WIjV6NGX/fzkmEZs6r1lQPQxrsUQumRb5Zv2Td3 +EVFeMh5mBsyvmRS3Ma+MwD4gmkw+aoc4sxQbvfXHGZegemGWFYpXVNNHw5d24jFE +H5+tWFh+nSc4Bmug77mxcGsbAJwUWOwMGEst/tHV31Z2RL8tDSsGLsa9owIDAQAB +AoGAasmbWzfMKBv4ntA0P1KwV54ebZk2Gc2HoIlneCIRaSKQAu0Z0iahi/myJYDD +/E6VuZQo18UxsJ1pMrRs3zyTNunD0hzVgnqz46nMGeMFdrsFcFQIFEtTtxngEyiy +zJrO+5oUKX6CIpRZhIBGWk0hKETm9WJ5LMPf48A9PcQGvgECQQD5k7NOy72adPt9 +9CSOIsaXeovu0ADmA6sDPTtCMzyXWiq6igN9q4gwBHmEfq/272l8CSfbVHAZL1ym +WtuLb0srAkEAy2JW3NgxNHn2DdcodEz7QBnRd7qO+qddNv+MoimsbFCpM+lUOXPn +IlFVA7IZYMDONwK+qHUIR8kWB2pKqGo7aQJACjqReMNE7BWrUQg2j1TBiufM4GbK +AqNX2PQjf50V+KYLZkXNytLC7CTizhlbIOXDDwBZD9YwGfgk9fR3VwmirQJBALbm +IKdJ5DYE17lqm/66m9fxX+YD50CR8cnb1mSehWiCwSbl1dA04s6BxaolJ51Sxh/C +YCKt3FxyAVV5yNnbbsECQQCZCrGcqIqFHuEYOhLMw0JGlRxVeR2PhWCaPX5M0s+9 +coyZRyO5MAWBfXDPF552Yqah1FRk+DX2qidkc27P+1QT +-----END RSA PRIVATE KEY----- diff --git a/packages/client/test/rpc/engine/kaustinen6.spec.ts b/packages/client/test/rpc/engine/kaustinen6.spec.ts index ce9041dc9b..99f58a5380 100644 --- a/packages/client/test/rpc/engine/kaustinen6.spec.ts +++ b/packages/client/test/rpc/engine/kaustinen6.spec.ts @@ -9,8 +9,9 @@ import genesisJSON from '../../testdata/geth-genesis/kaustinen6.json' import { getRpcClient, setupChain } from '../helpers.js' import type { Chain } from '../../../src/blockchain/index.js' -import type { BeaconPayloadJson, VerkleExecutionWitness } from '@ethereumjs/block' +import type { BeaconPayloadJson } from '@ethereumjs/block' import type { Common } from '@ethereumjs/common' +import type { VerkleExecutionWitness } from '@ethereumjs/util' import type { HttpClient } from 'jayson/promise' const genesisVerkleStateRoot = '0x1fbf85345a3cbba9a6d44f991b721e55620a22397c2a93ee8d5011136ac300ee' const genesisVerkleBlockHash = '0x3fe165c03e7a77d1e3759362ebeeb16fd964cb411ce11fbe35c7032fab5b9a8a' diff --git a/packages/client/test/rpc/rpc.spec.ts b/packages/client/test/rpc/rpc.spec.ts index e83f038833..103f40b654 100644 --- a/packages/client/test/rpc/rpc.spec.ts +++ b/packages/client/test/rpc/rpc.spec.ts @@ -1,13 +1,15 @@ import { randomBytes } from '@ethereumjs/util' import { Client } from 'jayson/promise' -import { encode } from 'jwt-simple' import { assert, describe, it } from 'vitest' +import { jwt } from '../../src/ext/jwt-simple.js' + import { createClient, createManager, getRpcClient, startRPC } from './helpers.js' -import type { TAlgorithm } from 'jwt-simple' +import type { TAlgorithm } from '../../src/ext/jwt-simple.js' import type { AddressInfo } from 'net' +const { encode } = jwt const jwtSecret = randomBytes(32) describe('JSON-RPC call', () => { diff --git a/packages/client/test/rpc/websocket.spec.ts b/packages/client/test/rpc/websocket.spec.ts index 1d30e6aa28..6dacd73197 100644 --- a/packages/client/test/rpc/websocket.spec.ts +++ b/packages/client/test/rpc/websocket.spec.ts @@ -1,16 +1,17 @@ import { randomBytes } from '@ethereumjs/util' import WebSocket from 'isomorphic-ws' import { Client } from 'jayson/promise' -import { encode } from 'jwt-simple' import { assert, describe, it } from 'vitest' +import { jwt } from '../../src/ext/jwt-simple.js' import { METHOD_NOT_FOUND } from '../../src/rpc/error-code.js' import { startRPC } from './helpers.js' -import type { TAlgorithm } from 'jwt-simple' +import type { TAlgorithm } from '../../src/ext/jwt-simple.js' const jwtSecret = randomBytes(32) +const { encode } = jwt describe('JSON-RPC call', () => { it('auth protected server with valid token', async () => { diff --git a/packages/common/src/interfaces.ts b/packages/common/src/interfaces.ts index c9731207cd..d3e1ac15bc 100644 --- a/packages/common/src/interfaces.ts +++ b/packages/common/src/interfaces.ts @@ -65,6 +65,64 @@ export type AccessListBytesItem = [Uint8Array, Uint8Array[]] export type AccessListBytes = AccessListBytesItem[] export type AccessList = AccessListItem[] +/** + * Verkle related + * + * Experimental (do not implement) + */ +export type AccessEventFlags = { + stemRead: boolean + stemWrite: boolean + chunkRead: boolean + chunkWrite: boolean + chunkFill: boolean +} + +/** + * Verkle related + * + * Experimental (do not implement) + */ +export interface AccessWitnessInterface { + touchAndChargeProofOfAbsence(address: Address): bigint + touchAndChargeMessageCall(address: Address): bigint + touchAndChargeValueTransfer(caller: Address, target: Address): bigint + touchAndChargeContractCreateInit(address: Address): bigint + touchAndChargeContractCreateCompleted(address: Address): bigint + touchTxOriginAndComputeGas(origin: Address): bigint + touchTxTargetAndComputeGas(target: Address, { sendsValue }: { sendsValue?: boolean }): bigint + touchCodeChunksRangeOnReadAndChargeGas(contact: Address, startPc: number, endPc: number): bigint + touchCodeChunksRangeOnWriteAndChargeGas(contact: Address, startPc: number, endPc: number): bigint + touchAddressOnWriteAndComputeGas( + address: Address, + treeIndex: number | bigint, + subIndex: number | Uint8Array + ): bigint + touchAddressOnReadAndComputeGas( + address: Address, + treeIndex: number | bigint, + subIndex: number | Uint8Array + ): bigint + touchAddressAndChargeGas( + address: Address, + treeIndex: number | bigint, + subIndex: number | Uint8Array, + { isWrite }: { isWrite?: boolean } + ): bigint + touchAddress( + address: Address, + treeIndex: number | bigint, + subIndex: number | Uint8Array, + { isWrite }: { isWrite?: boolean } + ): AccessEventFlags + shallowCopy(): AccessWitnessInterface + merge(accessWitness: AccessWitnessInterface): void +} + +/* + * Generic StateManager interface corresponding with the @ethereumjs/statemanager package + * + */ export interface StateManagerInterface { getAccount(address: Address): Promise putAccount(address: Address, account?: Account): Promise @@ -85,6 +143,13 @@ export interface StateManagerInterface { hasStateRoot(root: Uint8Array): Promise // only used in client shallowCopy(downlevelCaches?: boolean): StateManagerInterface getAppliedKey?(address: Uint8Array): Uint8Array + + /* + * The following optional methods are Verkle related + * + * Experimental (do not implement) + */ + checkChunkWitnessPresent?(contract: Address, programCounter: number): Promise } export interface EVMStateManagerInterface extends StateManagerInterface { diff --git a/packages/devp2p/package.json b/packages/devp2p/package.json index 3d7c4854ca..965d026ab6 100644 --- a/packages/devp2p/package.json +++ b/packages/devp2p/package.json @@ -60,7 +60,7 @@ "@ethereumjs/common": "^4.3.0", "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/util": "^9.0.3", - "@scure/base": "1.1.1", + "@scure/base": "^1.1.7", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", "lru-cache": "10.1.0", diff --git a/packages/evm/.gitignore b/packages/evm/.gitignore index 3ed4090b20..b1b31791f7 100644 --- a/packages/evm/.gitignore +++ b/packages/evm/.gitignore @@ -1,2 +1,4 @@ .cachedb benchmarks/*.js +# Bundle stats generated with npm visualize:bundle +stats.html diff --git a/packages/evm/package.json b/packages/evm/package.json index 501da5915f..fba80237d7 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -50,18 +50,18 @@ "test": "npm run test:node && npm run test:browser", "test:browser": "npx vitest run --config=../../config/vitest.browser.config.mts", "test:node": "npx vitest run", - "tsc": "../../config/cli/ts-compile.sh" + "tsc": "../../config/cli/ts-compile.sh", + "visualize:bundle": "npx vite build" }, "dependencies": { "@ethereumjs/common": "^4.3.0", "@ethereumjs/statemanager": "^2.3.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", - "@ethereumjs/verkle": "^0.0.2", "@types/debug": "^4.1.9", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", - "mcl-wasm": "^1.4.0", + "mcl-wasm": "^1.5.0", "rustbn-wasm": "^0.4.0" }, "devDependencies": { @@ -70,12 +70,14 @@ "@types/core-js": "^2.5.0", "@types/minimist": "^1.2.2", "@types/node-dir": "^0.0.34", + "@types/rollup-plugin-visualizer": "^4.2.4", "benchmark": "^2.1.4", "kzg-wasm": "^0.4.0", "level": "^8.0.0", "memory-level": "^1.0.0", "minimist": "^1.2.5", "node-dir": "^0.1.17", + "rollup-plugin-visualizer": "^5.12.0", "solc": "^0.8.1" }, "engines": { diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index b5c592ba35..bba5c9ce90 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -23,8 +23,7 @@ import type { EVM } from './evm.js' import type { Journal } from './journal.js' import type { AsyncOpHandler, Opcode, OpcodeMapEntry } from './opcodes/index.js' import type { Block, Blockchain, EVMProfilerOpts, EVMResult, Log } from './types.js' -import type { Common, EVMStateManagerInterface } from '@ethereumjs/common' -import type { AccessWitness, StatelessVerkleStateManager } from '@ethereumjs/statemanager' +import type { AccessWitnessInterface, Common, EVMStateManagerInterface } from '@ethereumjs/common' import type { Address, PrefixedHexString } from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault @@ -68,7 +67,7 @@ export interface Env { containerCode?: Uint8Array /** Full container code for EOF1 contracts */ blobVersionedHashes: Uint8Array[] /** Versioned hashes for blob transactions */ createdAddresses?: Set - accessWitness?: AccessWitness + accessWitness?: AccessWitnessInterface chargeCodeAccesses?: boolean } @@ -272,9 +271,7 @@ export class Interpreter { const contract = this._runState.interpreter.getAddress() if ( - !(await ( - this._runState.stateManager as StatelessVerkleStateManager - ).checkChunkWitnessPresent(contract, programCounter)) + !(await this._runState.stateManager.checkChunkWitnessPresent!(contract, programCounter)) ) { throw Error(`Invalid witness with missing codeChunk for pc=${programCounter}`) } diff --git a/packages/evm/src/message.ts b/packages/evm/src/message.ts index 0b7c60a482..85cefdf583 100644 --- a/packages/evm/src/message.ts +++ b/packages/evm/src/message.ts @@ -1,7 +1,7 @@ import { Address, BIGINT_0 } from '@ethereumjs/util' import type { PrecompileFunc } from './precompiles/index.js' -import type { AccessWitness } from '@ethereumjs/statemanager' +import type { AccessWitnessInterface } from '@ethereumjs/common' import type { PrefixedHexString } from '@ethereumjs/util' const defaults = { @@ -39,7 +39,7 @@ interface MessageOpts { authcallOrigin?: Address gasRefund?: bigint blobVersionedHashes?: Uint8Array[] - accessWitness?: AccessWitness + accessWitness?: AccessWitnessInterface } export class Message { @@ -75,7 +75,7 @@ export class Message { * List of versioned hashes if message is a blob transaction in the outer VM */ blobVersionedHashes?: Uint8Array[] - accessWitness?: AccessWitness + accessWitness?: AccessWitnessInterface constructor(opts: MessageOpts) { this.to = opts.to diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 049046a29f..fcaef0b5c2 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -20,18 +20,18 @@ import { MAX_INTEGER_BIGINT, SECP256K1_ORDER_DIV_2, TWO_POW256, + bigIntToAddressBytes, bigIntToBytes, - bigIntToHex, bytesToBigInt, bytesToHex, concatBytes, ecrecover, + getVerkleTreeIndexesForStorageSlot, hexToBytes, publicToAddress, setLengthLeft, setLengthRight, } from '@ethereumjs/util' -import { getTreeIndexesForStorageSlot } from '@ethereumjs/verkle' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { ERROR } from '../exceptions.js' @@ -622,13 +622,13 @@ export const handlers: Map = new Map([ return } - const historyAddress = Address.fromString( - bigIntToHex(common.param('vm', 'historyStorageAddress')) + const historyAddress = new Address( + bigIntToAddressBytes(common.param('vm', 'historyStorageAddress')) ) const key = setLengthLeft(bigIntToBytes(number % historyServeWindow), 32) if (common.isActivatedEIP(6800)) { - const { treeIndex, subIndex } = getTreeIndexesForStorageSlot(number) + const { treeIndex, subIndex } = getVerkleTreeIndexesForStorageSlot(number) // create witnesses and charge gas const statelessGas = runState.env.accessWitness!.touchAddressOnReadAndComputeGas( historyAddress, @@ -1156,7 +1156,8 @@ export const handlers: Map = new Map([ return } - const expectedAddress = new Address(setLengthLeft(bigIntToBytes(authority), 20).slice(-20)) + // we don't want strick check here on authority being in address range just last 20 bytes + const expectedAddress = new Address(bigIntToAddressBytes(authority, false)) const account = (await runState.stateManager.getAccount(expectedAddress)) ?? new Account() if (account.isContract()) { diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index 0ed6688aea..f5610f3d26 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -7,16 +7,14 @@ import { BIGINT_3, BIGINT_31, BIGINT_32, + VERKLE_BALANCE_LEAF_KEY, + VERKLE_CODE_HASH_LEAF_KEY, + VERKLE_CODE_SIZE_LEAF_KEY, + VERKLE_VERSION_LEAF_KEY, bigIntToBytes, + getVerkleTreeIndexesForStorageSlot, setLengthLeft, } from '@ethereumjs/util' -import { - BALANCE_LEAF_KEY, - CODE_HASH_LEAF_KEY, - CODE_SIZE_LEAF_KEY, - VERSION_LEAF_KEY, - getTreeIndexesForStorageSlot, -} from '@ethereumjs/verkle' import { ERROR } from '../exceptions.js' @@ -96,7 +94,7 @@ export const dynamicGasHandlers: Map BIGINT_0) { gas += runState.env.accessWitness!.touchAddressOnWriteAndComputeGas( contractAddress, 0, - BALANCE_LEAF_KEY + VERKLE_BALANCE_LEAF_KEY ) } @@ -876,14 +874,14 @@ export const dynamicGasHandlers: Map BIGINT_0) { selfDestructToColdAccessGas += runState.env.accessWitness!.touchAddressOnWriteAndComputeGas( selfdestructToAddress, 0, - BALANCE_LEAF_KEY + VERKLE_BALANCE_LEAF_KEY ) } diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 0621bab859..37ce910079 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -7,8 +7,7 @@ import type { AsyncDynamicGasHandler, SyncDynamicGasHandler } from './opcodes/ga import type { OpHandler } from './opcodes/index.js' import type { CustomPrecompile } from './precompiles/index.js' import type { PrecompileFunc } from './precompiles/types.js' -import type { Common, EVMStateManagerInterface } from '@ethereumjs/common' -import type { AccessWitness } from '@ethereumjs/statemanager' +import type { AccessWitnessInterface, Common, EVMStateManagerInterface } from '@ethereumjs/common' import type { Account, Address, AsyncEventEmitter, PrefixedHexString } from '@ethereumjs/util' export type DeleteOpcode = { @@ -124,7 +123,7 @@ export interface EVMRunCallOpts extends EVMRunOpts { */ message?: Message - accessWitness?: AccessWitness + accessWitness?: AccessWitnessInterface } interface NewContractEvent { diff --git a/packages/evm/vite.config.ts b/packages/evm/vite.config.ts index e620c08fb8..198b7cc30a 100644 --- a/packages/evm/vite.config.ts +++ b/packages/evm/vite.config.ts @@ -1,7 +1,21 @@ import wasm from 'vite-plugin-wasm' import topLevelAwait from 'vite-plugin-top-level-await' import { defineConfig } from 'vitest/config' +import { visualizer } from 'rollup-plugin-visualizer' export default defineConfig({ - plugins: [wasm(), topLevelAwait()], + plugins: [wasm(), topLevelAwait(), visualizer({ open: true, gzipSize: true })], + build: { + rollupOptions: { + // We choose safest to get worst case values + treeshake: 'safest', + }, + lib: { + entry: 'src/index.ts', + name: '@ethereumjs/evm', + fileName: (format) => `ethereumjs-evm-bundle.${format}.js`, + // only build for es + formats: ['es'], + }, + }, }) diff --git a/packages/statemanager/package.json b/packages/statemanager/package.json index 9849f958e6..8dd2457e11 100644 --- a/packages/statemanager/package.json +++ b/packages/statemanager/package.json @@ -53,17 +53,16 @@ "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/trie": "^6.2.0", "@ethereumjs/util": "^9.0.3", - "@ethereumjs/verkle": "^0.0.2", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", "js-sdsl": "^4.1.4", - "lru-cache": "10.1.0", - "verkle-cryptography-wasm": "^0.4.2" + "lru-cache": "10.1.0" }, "devDependencies": { "@ethereumjs/block": "^5.2.0", "@ethereumjs/genesis": "^0.2.2", "@types/debug": "^4.1.9", - "rustbn-wasm": "^0.4.0" + "rustbn-wasm": "^0.4.0", + "verkle-cryptography-wasm": "^0.4.5" } } diff --git a/packages/statemanager/src/accessWitness.ts b/packages/statemanager/src/accessWitness.ts index 916a922277..6ab4f87d4d 100644 --- a/packages/statemanager/src/accessWitness.ts +++ b/packages/statemanager/src/accessWitness.ts @@ -1,22 +1,26 @@ -import { BIGINT_0, bytesToBigInt, bytesToHex, hexToBytes, intToBytes } from '@ethereumjs/util' import { - BALANCE_LEAF_KEY, - CODE_HASH_LEAF_KEY, - CODE_OFFSET, - CODE_SIZE_LEAF_KEY, - HEADER_STORAGE_OFFSET, - MAIN_STORAGE_OFFSET, - NONCE_LEAF_KEY, + BIGINT_0, + VERKLE_BALANCE_LEAF_KEY, + VERKLE_CODE_HASH_LEAF_KEY, + VERKLE_CODE_OFFSET, + VERKLE_CODE_SIZE_LEAF_KEY, + VERKLE_HEADER_STORAGE_OFFSET, + VERKLE_MAIN_STORAGE_OFFSET, VERKLE_NODE_WIDTH, - VERSION_LEAF_KEY, - getKey, - getStem, - getTreeIndicesForCodeChunk, -} from '@ethereumjs/verkle' + VERKLE_NONCE_LEAF_KEY, + VERKLE_VERSION_LEAF_KEY, + bytesToBigInt, + bytesToHex, + getVerkleKey, + getVerkleStem, + getVerkleTreeIndicesForCodeChunk, + hexToBytes, + intToBytes, +} from '@ethereumjs/util' import debugDefault from 'debug' -import type { Address, PrefixedHexString } from '@ethereumjs/util' -import type { VerkleCrypto } from '@ethereumjs/verkle' +import type { AccessEventFlags, AccessWitnessInterface } from '@ethereumjs/common' +import type { Address, PrefixedHexString, VerkleCrypto } from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault const debug = createDebugLogger('statemanager:verkle:aw') @@ -36,14 +40,6 @@ type StemAccessEvent = { write?: boolean } // in upcoming iterations type ChunkAccessEvent = StemAccessEvent & { fill?: boolean } -type AccessEventFlags = { - stemRead: boolean - stemWrite: boolean - chunkRead: boolean - chunkWrite: boolean - chunkFill: boolean -} - // Since stem is pedersen hashed, it is useful to maintain the reverse relationship type StemMeta = { address: Address; treeIndex: number | bigint } type RawAccessedState = { @@ -72,7 +68,7 @@ export type AccessedStateWithAddress = AccessedState & { chunkKey: PrefixedHexString } -export class AccessWitness { +export class AccessWitness implements AccessWitnessInterface { stems: Map chunks: Map verkleCrypto: VerkleCrypto @@ -94,11 +90,11 @@ export class AccessWitness { touchAndChargeProofOfAbsence(address: Address): bigint { let gas = BIGINT_0 - gas += this.touchAddressOnReadAndComputeGas(address, 0, VERSION_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(address, 0, BALANCE_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(address, 0, CODE_SIZE_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(address, 0, CODE_HASH_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(address, 0, NONCE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_VERSION_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_BALANCE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_CODE_SIZE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_CODE_HASH_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_NONCE_LEAF_KEY) return gas } @@ -106,8 +102,8 @@ export class AccessWitness { touchAndChargeMessageCall(address: Address): bigint { let gas = BIGINT_0 - gas += this.touchAddressOnReadAndComputeGas(address, 0, VERSION_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(address, 0, CODE_SIZE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_VERSION_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_CODE_SIZE_LEAF_KEY) return gas } @@ -115,8 +111,8 @@ export class AccessWitness { touchAndChargeValueTransfer(caller: Address, target: Address): bigint { let gas = BIGINT_0 - gas += this.touchAddressOnWriteAndComputeGas(caller, 0, BALANCE_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(target, 0, BALANCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(caller, 0, VERKLE_BALANCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(target, 0, VERKLE_BALANCE_LEAF_KEY) return gas } @@ -124,8 +120,8 @@ export class AccessWitness { touchAndChargeContractCreateInit(address: Address): bigint { let gas = BIGINT_0 - gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERSION_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(address, 0, NONCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_VERSION_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_NONCE_LEAF_KEY) return gas } @@ -133,11 +129,11 @@ export class AccessWitness { touchAndChargeContractCreateCompleted(address: Address): bigint { let gas = BIGINT_0 - gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERSION_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(address, 0, BALANCE_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(address, 0, CODE_SIZE_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(address, 0, CODE_HASH_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(address, 0, NONCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_VERSION_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_BALANCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_CODE_SIZE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_CODE_HASH_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_NONCE_LEAF_KEY) return gas } @@ -145,12 +141,12 @@ export class AccessWitness { touchTxOriginAndComputeGas(origin: Address): bigint { let gas = BIGINT_0 - gas += this.touchAddressOnReadAndComputeGas(origin, 0, VERSION_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(origin, 0, CODE_SIZE_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(origin, 0, CODE_HASH_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(origin, 0, VERKLE_VERSION_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(origin, 0, VERKLE_CODE_SIZE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(origin, 0, VERKLE_CODE_HASH_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(origin, 0, NONCE_LEAF_KEY) - gas += this.touchAddressOnWriteAndComputeGas(origin, 0, BALANCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(origin, 0, VERKLE_NONCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(origin, 0, VERKLE_BALANCE_LEAF_KEY) return gas } @@ -158,15 +154,15 @@ export class AccessWitness { touchTxTargetAndComputeGas(target: Address, { sendsValue }: { sendsValue?: boolean } = {}) { let gas = BIGINT_0 - gas += this.touchAddressOnReadAndComputeGas(target, 0, VERSION_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(target, 0, CODE_SIZE_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(target, 0, CODE_HASH_LEAF_KEY) - gas += this.touchAddressOnReadAndComputeGas(target, 0, NONCE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(target, 0, VERKLE_VERSION_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(target, 0, VERKLE_CODE_SIZE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(target, 0, VERKLE_CODE_HASH_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(target, 0, VERKLE_NONCE_LEAF_KEY) if (sendsValue === true) { - gas += this.touchAddressOnWriteAndComputeGas(target, 0, BALANCE_LEAF_KEY) + gas += this.touchAddressOnWriteAndComputeGas(target, 0, VERKLE_BALANCE_LEAF_KEY) } else { - gas += this.touchAddressOnReadAndComputeGas(target, 0, BALANCE_LEAF_KEY) + gas += this.touchAddressOnReadAndComputeGas(target, 0, VERKLE_BALANCE_LEAF_KEY) } return gas @@ -175,7 +171,7 @@ export class AccessWitness { touchCodeChunksRangeOnReadAndChargeGas(contact: Address, startPc: number, endPc: number): bigint { let gas = BIGINT_0 for (let chunkNum = Math.floor(startPc / 31); chunkNum <= Math.floor(endPc / 31); chunkNum++) { - const { treeIndex, subIndex } = getTreeIndicesForCodeChunk(chunkNum) + const { treeIndex, subIndex } = getVerkleTreeIndicesForCodeChunk(chunkNum) gas += this.touchAddressOnReadAndComputeGas(contact, treeIndex, subIndex) } return gas @@ -188,7 +184,7 @@ export class AccessWitness { ): bigint { let gas = BIGINT_0 for (let chunkNum = Math.floor(startPc / 31); chunkNum <= Math.floor(endPc / 31); chunkNum++) { - const { treeIndex, subIndex } = getTreeIndicesForCodeChunk(chunkNum) + const { treeIndex, subIndex } = getVerkleTreeIndicesForCodeChunk(chunkNum) gas += this.touchAddressOnWriteAndComputeGas(contact, treeIndex, subIndex) } return gas @@ -225,20 +221,20 @@ export class AccessWitness { { isWrite } ) - if (stemRead) { + if (stemRead === true) { gas += WitnessBranchReadCost } - if (stemWrite) { + if (stemWrite === true) { gas += WitnessBranchWriteCost } - if (chunkRead) { + if (chunkRead === true) { gas += WitnessChunkReadCost } - if (chunkWrite) { + if (chunkWrite === true) { gas += WitnessChunkWriteCost } - if (chunkFill) { + if (chunkFill === true) { gas += WitnessChunkFillCost } @@ -263,7 +259,7 @@ export class AccessWitness { // i.e. no fill cost is charged right now const chunkFill = false - const accessedStemKey = getStem(this.verkleCrypto, address, treeIndex) + const accessedStemKey = getVerkleStem(this.verkleCrypto, address, treeIndex) const accessedStemHex = bytesToHex(accessedStemKey) let accessedStem = this.stems.get(accessedStemHex) if (accessedStem === undefined) { @@ -272,7 +268,7 @@ export class AccessWitness { this.stems.set(accessedStemHex, accessedStem) } - const accessedChunkKey = getKey( + const accessedChunkKey = getVerkleKey( accessedStemKey, typeof subIndex === 'number' ? intToBytes(subIndex) : subIndex ) @@ -372,18 +368,18 @@ export function decodeAccessedState(treeIndex: number | bigint, chunkIndex: numb case BigInt(4): return { type: AccessedStateType.CodeSize } default: - if (position < HEADER_STORAGE_OFFSET) { - throw Error(`No attribute yet stored >=5 and <${HEADER_STORAGE_OFFSET}`) + if (position < VERKLE_HEADER_STORAGE_OFFSET) { + throw Error(`No attribute yet stored >=5 and <${VERKLE_HEADER_STORAGE_OFFSET}`) } - if (position >= HEADER_STORAGE_OFFSET && position < CODE_OFFSET) { - const slot = position - BigInt(HEADER_STORAGE_OFFSET) + if (position >= VERKLE_HEADER_STORAGE_OFFSET && position < VERKLE_CODE_OFFSET) { + const slot = position - BigInt(VERKLE_HEADER_STORAGE_OFFSET) return { type: AccessedStateType.Storage, slot } - } else if (position >= CODE_OFFSET && position < MAIN_STORAGE_OFFSET) { - const codeChunkIdx = Number(position) - CODE_OFFSET + } else if (position >= VERKLE_CODE_OFFSET && position < VERKLE_MAIN_STORAGE_OFFSET) { + const codeChunkIdx = Number(position) - VERKLE_CODE_OFFSET return { type: AccessedStateType.Code, codeOffset: codeChunkIdx * 31 } - } else if (position >= MAIN_STORAGE_OFFSET) { - const slot = BigInt(position - MAIN_STORAGE_OFFSET) + } else if (position >= VERKLE_MAIN_STORAGE_OFFSET) { + const slot = BigInt(position - VERKLE_MAIN_STORAGE_OFFSET) return { type: AccessedStateType.Storage, slot } } else { throw Error( diff --git a/packages/statemanager/src/statelessVerkleStateManager.ts b/packages/statemanager/src/statelessVerkleStateManager.ts index 2b8d88b12f..b696af12f9 100644 --- a/packages/statemanager/src/statelessVerkleStateManager.ts +++ b/packages/statemanager/src/statelessVerkleStateManager.ts @@ -2,28 +2,25 @@ import { Account, KECCAK256_NULL, KECCAK256_NULL_S, + VerkleLeafType, bigIntToBytes, bytesToBigInt, bytesToHex, bytesToInt32, + getVerkleKey, + getVerkleStem, + getVerkleTreeKeyForCodeChunk, + getVerkleTreeKeyForStorageSlot, hexToBytes, padToEven, setLengthLeft, setLengthRight, short, toBytes, + verifyVerkleProof, } from '@ethereumjs/util' -import { - LeafType, - getKey, - getStem, - getTreeKeyForCodeChunk, - getTreeKeyForStorageSlot, - verifyProof, -} from '@ethereumjs/verkle' import debugDefault from 'debug' import { keccak256 } from 'ethereum-cryptography/keccak.js' -import { loadVerkleCrypto } from 'verkle-cryptography-wasm' import { AccessWitness, AccessedStateType, decodeValue } from './accessWitness.js' import { AccountCache, CacheType, CodeCache, StorageCache } from './cache/index.js' @@ -31,7 +28,6 @@ import { OriginalStorageCache } from './cache/originalStorageCache.js' import type { AccessedStateWithAddress } from './accessWitness.js' import type { DefaultStateManager } from './stateManager.js' -import type { VerkleExecutionWitness, VerkleProof } from '@ethereumjs/block' import type { AccountFields, Common, @@ -40,8 +36,13 @@ import type { StorageDump, StorageRange, } from '@ethereumjs/common' -import type { Address, PrefixedHexString } from '@ethereumjs/util' -import type { VerkleCrypto } from '@ethereumjs/verkle' +import type { + Address, + PrefixedHexString, + VerkleCrypto, + VerkleExecutionWitness, + VerkleProof, +} from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault @@ -110,7 +111,7 @@ export interface StatelessVerkleStateManagerOpts { storageCacheOpts?: CacheOptions codeCacheOpts?: CacheOptions accesses?: AccessWitness - verkleCrypto?: VerkleCrypto + verkleCrypto: VerkleCrypto initialStateRoot?: Uint8Array } @@ -175,21 +176,10 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { private keccakFunction: Function - /** - * Async static constructor for StatelessVerkleStateManager - * @param opts `StatelessVerkleStateManagerOpts` - * @returns a StatelessVerkleStateManager with initialized Verkle Crypto - */ - static create = async (opts: StatelessVerkleStateManagerOpts = {}) => { - if (opts.verkleCrypto === undefined) { - opts.verkleCrypto = await loadVerkleCrypto() - } - return new StatelessVerkleStateManager(opts) - } /** * Instantiate the StateManager interface. */ - constructor(opts: StatelessVerkleStateManagerOpts = {}) { + constructor(opts: StatelessVerkleStateManagerOpts) { this.originalStorageCache = new OriginalStorageCache(this.getContractStorage.bind(this)) this._accountCacheSettings = { @@ -320,7 +310,9 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { async checkChunkWitnessPresent(address: Address, codeOffset: number) { const chunkId = codeOffset / 31 - const chunkKey = bytesToHex(await getTreeKeyForCodeChunk(address, chunkId, this.verkleCrypto)) + const chunkKey = bytesToHex( + await getVerkleTreeKeyForCodeChunk(address, chunkId, this.verkleCrypto) + ) return this._state[chunkKey] !== undefined } @@ -391,7 +383,9 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { const chunks = Math.floor(codeSize / 31) + 1 for (let chunkId = 0; chunkId < chunks; chunkId++) { - const chunkKey = bytesToHex(await getTreeKeyForCodeChunk(address, chunkId, this.verkleCrypto)) + const chunkKey = bytesToHex( + await getVerkleTreeKeyForCodeChunk(address, chunkId, this.verkleCrypto) + ) const codeChunk = this._state[chunkKey] if (codeChunk === null) { const errorMsg = `Invalid access to a non existent code chunk with chunkKey=${chunkKey}` @@ -463,7 +457,7 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { } } - const storageKey = await getTreeKeyForStorageSlot( + const storageKey = await getVerkleTreeKeyForStorageSlot( address, BigInt(bytesToHex(key)), this.verkleCrypto @@ -489,7 +483,7 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { this._storageCache!.put(address, key, value) } else { // TODO: Consider refactoring this in a writeContractStorage function? Like in stateManager.ts - const storageKey = await getTreeKeyForStorageSlot( + const storageKey = await getVerkleTreeKeyForStorageSlot( address, BigInt(bytesToHex(key)), this.verkleCrypto @@ -505,8 +499,8 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { * @param address - Address to clear the storage of */ async clearContractStorage(address: Address): Promise { - const stem = getStem(this.verkleCrypto, address, 0) - const codeHashKey = getKey(stem, LeafType.CodeHash) + const stem = getVerkleStem(this.verkleCrypto, address, 0) + const codeHashKey = getVerkleKey(stem, VerkleLeafType.CodeHash) this._storageCache?.clearContractStorage(address) // Update codeHash to `c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470` this._state[bytesToHex(codeHashKey)] = KECCAK256_NULL_S @@ -522,12 +516,12 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { } } - const stem = getStem(this.verkleCrypto, address, 0) - const versionKey = getKey(stem, LeafType.Version) - const balanceKey = getKey(stem, LeafType.Balance) - const nonceKey = getKey(stem, LeafType.Nonce) - const codeHashKey = getKey(stem, LeafType.CodeHash) - const codeSizeKey = getKey(stem, LeafType.CodeSize) + const stem = getVerkleStem(this.verkleCrypto, address, 0) + const versionKey = getVerkleKey(stem, VerkleLeafType.Version) + const balanceKey = getVerkleKey(stem, VerkleLeafType.Balance) + const nonceKey = getVerkleKey(stem, VerkleLeafType.Nonce) + const codeHashKey = getVerkleKey(stem, VerkleLeafType.CodeHash) + const codeSizeKey = getVerkleKey(stem, VerkleLeafType.CodeSize) const versionRaw = this._state[bytesToHex(versionKey)] const balanceRaw = this._state[bytesToHex(balanceKey)] @@ -609,10 +603,10 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { } if (this._accountCacheSettings.deactivate) { - const stem = getStem(this.verkleCrypto, address, 0) - const balanceKey = getKey(stem, LeafType.Balance) - const nonceKey = getKey(stem, LeafType.Nonce) - const codeHashKey = getKey(stem, LeafType.CodeHash) + const stem = getVerkleStem(this.verkleCrypto, address, 0) + const balanceKey = getVerkleKey(stem, VerkleLeafType.Balance) + const nonceKey = getVerkleKey(stem, VerkleLeafType.Nonce) + const codeHashKey = getVerkleKey(stem, VerkleLeafType.CodeHash) const balanceBuf = setLengthRight(bigIntToBytes(account.balance, true), 32) const nonceBuf = setLengthRight(bigIntToBytes(account.nonce, true), 32) @@ -673,7 +667,7 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface { return false } - return verifyProof(this.verkleCrypto, stateRoot, this._executionWitness) + return verifyVerkleProof(this.verkleCrypto, stateRoot, this._executionWitness) } // Verifies that the witness post-state matches the computed post-state diff --git a/packages/statemanager/test/statelessVerkleStateManager.spec.ts b/packages/statemanager/test/statelessVerkleStateManager.spec.ts index 9642d15a7f..94270c9128 100644 --- a/packages/statemanager/test/statelessVerkleStateManager.spec.ts +++ b/packages/statemanager/test/statelessVerkleStateManager.spec.ts @@ -4,12 +4,14 @@ import { TransactionFactory } from '@ethereumjs/tx' import { Account, Address, + VerkleLeafType, bytesToBigInt, bytesToHex, + getVerkleKey, + getVerkleStem, hexToBytes, randomBytes, } from '@ethereumjs/util' -import { LeafType, getKey, getStem } from '@ethereumjs/verkle' import { loadVerkleCrypto } from 'verkle-cryptography-wasm' import { assert, beforeAll, describe, it, test } from 'vitest' @@ -19,8 +21,7 @@ import * as testnetVerkleKaustinen from './testdata/testnetVerkleKaustinen.json' import * as verkleBlockJSON from './testdata/verkleKaustinen6Block72.json' import type { BlockData } from '@ethereumjs/block' -import type { PrefixedHexString } from '@ethereumjs/util' -import type { VerkleCrypto } from '@ethereumjs/verkle' +import type { PrefixedHexString, VerkleCrypto } from '@ethereumjs/util' describe('StatelessVerkleStateManager: Kaustinen Verkle Block', () => { let verkleCrypto: VerkleCrypto @@ -39,14 +40,14 @@ describe('StatelessVerkleStateManager: Kaustinen Verkle Block', () => { }) it('initPreState()', async () => { - const stateManager = await StatelessVerkleStateManager.create({ verkleCrypto }) + const stateManager = new StatelessVerkleStateManager({ verkleCrypto }) stateManager.initVerkleExecutionWitness(block.header.number, block.executionWitness) assert.ok(Object.keys(stateManager['_state']).length !== 0, 'should initialize with state') }) it('getAccount()', async () => { - const stateManager = await StatelessVerkleStateManager.create({ common, verkleCrypto }) + const stateManager = new StatelessVerkleStateManager({ common, verkleCrypto }) stateManager.initVerkleExecutionWitness(block.header.number, block.executionWitness) const account = await stateManager.getAccount( @@ -64,7 +65,7 @@ describe('StatelessVerkleStateManager: Kaustinen Verkle Block', () => { }) it('put/delete/modify account', async () => { - const stateManager = await StatelessVerkleStateManager.create({ common, verkleCrypto }) + const stateManager = new StatelessVerkleStateManager({ common, verkleCrypto }) stateManager.initVerkleExecutionWitness(block.header.number, block.executionWitness) const address = new Address(randomBytes(20)) @@ -110,15 +111,15 @@ describe('StatelessVerkleStateManager: Kaustinen Verkle Block', () => { }) it('getKey function', async () => { - const stateManager = await StatelessVerkleStateManager.create({ common, verkleCrypto }) + const stateManager = new StatelessVerkleStateManager({ common, verkleCrypto }) stateManager.initVerkleExecutionWitness(block.header.number, block.executionWitness) const address = Address.fromString('0x6177843db3138ae69679a54b95cf345ed759450d') - const stem = getStem(stateManager.verkleCrypto, address, 0n) + const stem = getVerkleStem(stateManager.verkleCrypto, address, 0n) - const balanceKey = getKey(stem, LeafType.Balance) - const nonceKey = getKey(stem, LeafType.Nonce) - const codeHashKey = getKey(stem, LeafType.CodeHash) + const balanceKey = getVerkleKey(stem, VerkleLeafType.Balance) + const nonceKey = getVerkleKey(stem, VerkleLeafType.Nonce) + const codeHashKey = getVerkleKey(stem, VerkleLeafType.CodeHash) const balanceRaw = stateManager['_state'][bytesToHex(balanceKey)] const nonceRaw = stateManager['_state'][bytesToHex(nonceKey)] @@ -168,7 +169,7 @@ describe('StatelessVerkleStateManager: Kaustinen Verkle Block', () => { // TODO contract storage functions not yet completely implemented test.skip('get/put/clear contract storage', async () => { - const stateManager = await StatelessVerkleStateManager.create({ common, verkleCrypto }) + const stateManager = new StatelessVerkleStateManager({ common, verkleCrypto }) stateManager.initVerkleExecutionWitness(block.header.number, block.executionWitness) const contractAddress = Address.fromString('0x4242424242424242424242424242424242424242') diff --git a/packages/util/src/address.ts b/packages/util/src/address.ts index 5fdf192df4..2cb37d498e 100644 --- a/packages/util/src/address.ts +++ b/packages/util/src/address.ts @@ -43,7 +43,7 @@ export class Address { */ static fromString(str: string): Address { if (!isValidAddress(str)) { - throw new Error('Invalid address') + throw new Error(`Invalid address input=${str}`) } return new Address(hexToBytes(str)) } diff --git a/packages/util/src/bytes.ts b/packages/util/src/bytes.ts index c67950c251..907bf2ad3d 100644 --- a/packages/util/src/bytes.ts +++ b/packages/util/src/bytes.ts @@ -430,6 +430,16 @@ export const bigIntToUnpaddedBytes = (value: bigint): Uint8Array => { return unpadBytes(bigIntToBytes(value)) } +export const bigIntToAddressBytes = (value: bigint, strict: boolean = true): Uint8Array => { + const addressBytes = bigIntToBytes(value) + if (strict && addressBytes.length > 20) { + throw Error(`Invalid address bytes length=${addressBytes.length} strict=${strict}`) + } + + // setLength already slices if more than requisite length + return setLengthLeft(addressBytes, 20) +} + /** * Convert value from number to an unpadded Uint8Array * (useful for RLP transport) diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 3b99e04a01..bc3e1462cf 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -65,3 +65,4 @@ export * from './lock.js' export * from './mapDB.js' export * from './provider.js' export * from './requests.js' +export * from './verkle.js' diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts index cc651113bd..9df5121b66 100644 --- a/packages/util/src/requests.ts +++ b/packages/util/src/requests.ts @@ -30,7 +30,7 @@ export type DepositRequestV1 = { export type WithdrawalRequestV1 = { sourceAddress: PrefixedHexString // DATA 20 bytes - validatorPublicKey: PrefixedHexString // DATA 48 bytes + validatorPubkey: PrefixedHexString // DATA 48 bytes amount: PrefixedHexString // QUANTITY 8 bytes in gwei } @@ -49,7 +49,7 @@ export type DepositRequestData = { export type WithdrawalRequestData = { sourceAddress: Uint8Array - validatorPublicKey: Uint8Array + validatorPubkey: Uint8Array amount: bigint } @@ -140,22 +140,22 @@ export class DepositRequest extends CLRequest { export class WithdrawalRequest extends CLRequest { constructor( public readonly sourceAddress: Uint8Array, - public readonly validatorPublicKey: Uint8Array, + public readonly validatorPubkey: Uint8Array, public readonly amount: bigint ) { super(CLRequestType.Withdrawal) } public static fromRequestData(withdrawalData: WithdrawalRequestData): WithdrawalRequest { - const { sourceAddress, validatorPublicKey, amount } = withdrawalData - return new WithdrawalRequest(sourceAddress, validatorPublicKey, amount) + const { sourceAddress, validatorPubkey, amount } = withdrawalData + return new WithdrawalRequest(sourceAddress, validatorPubkey, amount) } public static fromJSON(jsonData: WithdrawalRequestV1): WithdrawalRequest { - const { sourceAddress, validatorPublicKey, amount } = jsonData + const { sourceAddress, validatorPubkey, amount } = jsonData return this.fromRequestData({ sourceAddress: hexToBytes(sourceAddress), - validatorPublicKey: hexToBytes(validatorPublicKey), + validatorPubkey: hexToBytes(validatorPubkey), amount: hexToBigInt(amount), }) } @@ -165,27 +165,27 @@ export class WithdrawalRequest extends CLRequest { return concatBytes( Uint8Array.from([this.type]), - RLP.encode([this.sourceAddress, this.validatorPublicKey, amountBytes]) + RLP.encode([this.sourceAddress, this.validatorPubkey, amountBytes]) ) } toJSON(): WithdrawalRequestV1 { return { sourceAddress: bytesToHex(this.sourceAddress), - validatorPublicKey: bytesToHex(this.validatorPublicKey), + validatorPubkey: bytesToHex(this.validatorPubkey), amount: bigIntToHex(this.amount), } } public static deserialize(bytes: Uint8Array): WithdrawalRequest { - const [sourceAddress, validatorPublicKey, amount] = RLP.decode(bytes.slice(1)) as [ + const [sourceAddress, validatorPubkey, amount] = RLP.decode(bytes.slice(1)) as [ Uint8Array, Uint8Array, Uint8Array ] return this.fromRequestData({ sourceAddress, - validatorPublicKey, + validatorPubkey, amount: bytesToBigInt(amount), }) } diff --git a/packages/util/src/verkle.ts b/packages/util/src/verkle.ts new file mode 100644 index 0000000000..959559f701 --- /dev/null +++ b/packages/util/src/verkle.ts @@ -0,0 +1,217 @@ +import { + bigIntToBytes, + bytesToHex, + concatBytes, + int32ToBytes, + intToBytes, + setLengthLeft, + setLengthRight, + toBytes, +} from './bytes.js' + +import type { Address } from './address.js' +import type { PrefixedHexString } from './types.js' + +/** + * Verkle related constants and helper functions + * + * Experimental (do not use in production!) + */ + +/* Verkle Crypto */ +export interface VerkleCrypto { + getTreeKey: (address: Uint8Array, treeIndex: Uint8Array, subIndex: number) => Uint8Array + getTreeKeyHash: (address: Uint8Array, treeIndexLE: Uint8Array) => Uint8Array + updateCommitment: ( + commitment: Uint8Array, + commitmentIndex: number, + oldScalarValue: Uint8Array, + newScalarValue: Uint8Array + ) => Uint8Array // Commitment + zeroCommitment: Uint8Array + verifyExecutionWitnessPreState: (prestateRoot: string, execution_witness_json: string) => boolean + hashCommitment: (commitment: Uint8Array) => Uint8Array + serializeCommitment: (commitment: Uint8Array) => Uint8Array +} + +/** + * @dev Returns the 31-bytes verkle tree stem for a given address and tree index. + * @dev Assumes that the verkle node width = 256 + * @param ffi The verkle ffi object from verkle-crypotography-wasm. + * @param address The address to generate the tree key for. + * @param treeIndex The index of the tree to generate the key for. Defaults to 0. + * @return The 31-bytes verkle tree stem as a Uint8Array. + */ +export function getVerkleStem( + ffi: VerkleCrypto, + address: Address, + treeIndex: number | bigint = 0 +): Uint8Array { + const address32 = setLengthLeft(address.toBytes(), 32) + + let treeIndexBytes: Uint8Array + if (typeof treeIndex === 'number') { + treeIndexBytes = setLengthRight(int32ToBytes(Number(treeIndex), true), 32) + } else { + treeIndexBytes = setLengthRight(bigIntToBytes(BigInt(treeIndex), true).slice(0, 32), 32) + } + + const treeStem = ffi.getTreeKey(address32, treeIndexBytes, 0).slice(0, 31) + + return treeStem +} + +/** + * Verifies that the executionWitness is valid for the given prestateRoot. + * @param ffi The verkle ffi object from verkle-crypotography-wasm. + * @param prestateRoot The prestateRoot matching the executionWitness. + * @param executionWitness The verkle execution witness. + * @returns {boolean} Whether or not the executionWitness belongs to the prestateRoot. + */ +export function verifyVerkleProof( + ffi: VerkleCrypto, + prestateRoot: Uint8Array, + executionWitness: VerkleExecutionWitness +): boolean { + return ffi.verifyExecutionWitnessPreState( + bytesToHex(prestateRoot), + JSON.stringify(executionWitness) + ) +} + +/* Verkle Structure */ + +export interface VerkleProof { + commitmentsByPath: PrefixedHexString[] + d: PrefixedHexString + depthExtensionPresent: PrefixedHexString + ipaProof: { + cl: PrefixedHexString[] + cr: PrefixedHexString[] + finalEvaluation: PrefixedHexString + } + otherStems: PrefixedHexString[] +} + +export interface VerkleStateDiff { + stem: PrefixedHexString + suffixDiffs: { + currentValue: PrefixedHexString | null + newValue: PrefixedHexString | null + suffix: number | string + }[] +} + +/** + * Experimental, object format could eventual change. + * An object that provides the state and proof necessary for verkle stateless execution + * */ +export interface VerkleExecutionWitness { + /** + * An array of state diffs. + * Each item corresponding to state accesses or state modifications of the block. + * In the current design, it also contains the resulting state of the block execution (post-state). + */ + stateDiff: VerkleStateDiff[] + /** + * The verkle proof for the block. + * Proves that the provided stateDiff belongs to the canonical verkle tree. + */ + verkleProof: VerkleProof +} + +export enum VerkleLeafType { + Version = 0, + Balance = 1, + Nonce = 2, + CodeHash = 3, + CodeSize = 4, +} + +export const VERKLE_VERSION_LEAF_KEY = intToBytes(VerkleLeafType.Version) +export const VERKLE_BALANCE_LEAF_KEY = intToBytes(VerkleLeafType.Balance) +export const VERKLE_NONCE_LEAF_KEY = intToBytes(VerkleLeafType.Nonce) +export const VERKLE_CODE_HASH_LEAF_KEY = intToBytes(VerkleLeafType.CodeHash) +export const VERKLE_CODE_SIZE_LEAF_KEY = intToBytes(VerkleLeafType.CodeSize) + +export const VERKLE_HEADER_STORAGE_OFFSET = 64 +export const VERKLE_CODE_OFFSET = 128 +export const VERKLE_NODE_WIDTH = 256 +export const VERKLE_MAIN_STORAGE_OFFSET = BigInt(256) ** BigInt(31) + +/** + * @dev Returns the tree key for a given verkle tree stem, and sub index. + * @dev Assumes that the verkle node width = 256 + * @param stem The 31-bytes verkle tree stem as a Uint8Array. + * @param subIndex The sub index of the tree to generate the key for as a Uint8Array. + * @return The tree key as a Uint8Array. + */ + +export const getVerkleKey = (stem: Uint8Array, leaf: VerkleLeafType | Uint8Array) => { + switch (leaf) { + case VerkleLeafType.Version: + return concatBytes(stem, VERKLE_VERSION_LEAF_KEY) + case VerkleLeafType.Balance: + return concatBytes(stem, VERKLE_BALANCE_LEAF_KEY) + case VerkleLeafType.Nonce: + return concatBytes(stem, VERKLE_NONCE_LEAF_KEY) + case VerkleLeafType.CodeHash: + return concatBytes(stem, VERKLE_CODE_HASH_LEAF_KEY) + case VerkleLeafType.CodeSize: + return concatBytes(stem, VERKLE_CODE_SIZE_LEAF_KEY) + default: + return concatBytes(stem, leaf) + } +} + +export function getVerkleTreeIndexesForStorageSlot(storageKey: bigint): { + treeIndex: bigint + subIndex: number +} { + let position: bigint + if (storageKey < VERKLE_CODE_OFFSET - VERKLE_HEADER_STORAGE_OFFSET) { + position = BigInt(VERKLE_HEADER_STORAGE_OFFSET) + storageKey + } else { + position = VERKLE_MAIN_STORAGE_OFFSET + storageKey + } + + const treeIndex = position / BigInt(VERKLE_NODE_WIDTH) + const subIndex = Number(position % BigInt(VERKLE_NODE_WIDTH)) + + return { treeIndex, subIndex } +} + +export function getVerkleTreeIndicesForCodeChunk(chunkId: number) { + const treeIndex = Math.floor((VERKLE_CODE_OFFSET + chunkId) / VERKLE_NODE_WIDTH) + const subIndex = (VERKLE_CODE_OFFSET + chunkId) % VERKLE_NODE_WIDTH + return { treeIndex, subIndex } +} + +export const getVerkleTreeKeyForCodeChunk = async ( + address: Address, + chunkId: number, + verkleCrypto: VerkleCrypto +) => { + const { treeIndex, subIndex } = getVerkleTreeIndicesForCodeChunk(chunkId) + return concatBytes(getVerkleStem(verkleCrypto, address, treeIndex), toBytes(subIndex)) +} + +export const chunkifyCode = (code: Uint8Array) => { + // Pad code to multiple of 31 bytes + if (code.length % 31 !== 0) { + const paddingLength = 31 - (code.length % 31) + code = setLengthRight(code, code.length + paddingLength) + } + + throw new Error('Not implemented') +} + +export const getVerkleTreeKeyForStorageSlot = async ( + address: Address, + storageKey: bigint, + verkleCrypto: VerkleCrypto +) => { + const { treeIndex, subIndex } = getVerkleTreeIndexesForStorageSlot(storageKey) + + return concatBytes(getVerkleStem(verkleCrypto, address, treeIndex), toBytes(subIndex)) +} diff --git a/packages/util/test/bytes.spec.ts b/packages/util/test/bytes.spec.ts index fa636fe43d..3d6954b2b6 100644 --- a/packages/util/test/bytes.spec.ts +++ b/packages/util/test/bytes.spec.ts @@ -3,6 +3,7 @@ import { assert, describe, it } from 'vitest' import { Address, addHexPrefix, + bigIntToAddressBytes, bigIntToBytes, bigIntToHex, bigIntToUnpaddedBytes, @@ -401,6 +402,36 @@ describe('bigIntToUnpaddedBytes', () => { }) }) +describe('bigIntToAddressBytes', () => { + const testCases = [ + [ + '0x0aae40965e6800cd9b1f4b05ff21581047e3f91e', + BigInt('0x0aae40965e6800cd9b1f4b05ff21581047e3f91e'), + true, + ], + [ + '0xe473f7e92ba2490e9fcbbe8bb9c3be3adbb74efc', + BigInt('0xe473f7e92ba2490e9fcbbe8bb9c3be3adbb74efc'), + true, + ], + [ + '0xae40965e6800cd9b1f4b05ff21581047e3f91e00', + BigInt('0x0aae40965e6800cd9b1f4b05ff21581047e3f91e00'), + false, + ], + ] + + for (const [addressHex, addressBigInt, isSafe] of testCases) { + it('should correctly convert', () => { + const addressHexFromBigInt = bytesToHex(bigIntToAddressBytes(addressBigInt, false)) + assert.equal(addressHex, addressHexFromBigInt, `should correctly convert ${addressBigInt}`) + if (isSafe === false) { + assert.throw(() => bigIntToAddressBytes(addressBigInt)) + } + }) + } +}) + describe('intToUnpaddedBytes', () => { it('should equal unpadded buffer value', () => { assert.deepEqual(intToUnpaddedBytes(0), Uint8Array.from([])) diff --git a/packages/util/test/requests.spec.ts b/packages/util/test/requests.spec.ts index 297fb30f41..5e798f98e4 100644 --- a/packages/util/test/requests.spec.ts +++ b/packages/util/test/requests.spec.ts @@ -38,7 +38,7 @@ describe('Requests', () => { it('withdrawal request', () => { const withdrawalRequestData = { sourceAddress: randomBytes(20), - validatorPublicKey: randomBytes(48), + validatorPubkey: randomBytes(48), amount: bytesToBigInt(randomBytes(8)), } diff --git a/packages/util/test/verkle.spec.ts b/packages/util/test/verkle.spec.ts new file mode 100644 index 0000000000..3a66f1244a --- /dev/null +++ b/packages/util/test/verkle.spec.ts @@ -0,0 +1,77 @@ +import { loadVerkleCrypto } from 'verkle-cryptography-wasm' +import { assert, beforeAll, describe, it } from 'vitest' + +import * as verkleBlockJSON from '../../statemanager/test/testdata/verkleKaustinen6Block72.json' +import { + Address, + type VerkleCrypto, + type VerkleExecutionWitness, + VerkleLeafType, + bytesToHex, + concatBytes, + getVerkleKey, + getVerkleStem, + hexToBytes, + intToBytes, + randomBytes, + verifyVerkleProof, +} from '../src/index.js' + +describe('Verkle cryptographic helpers', () => { + let verkle: VerkleCrypto + beforeAll(async () => { + verkle = await loadVerkleCrypto() + }) + + it('getVerkleStem(): returns the expected stems', () => { + // Empty address + assert.equal( + bytesToHex( + getVerkleStem(verkle, Address.fromString('0x0000000000000000000000000000000000000000')) + ), + '0x1a100684fd68185060405f3f160e4bb6e034194336b547bdae323f888d5332' + ) + + // Non-empty address + assert.equal( + bytesToHex( + getVerkleStem(verkle, Address.fromString('0x71562b71999873DB5b286dF957af199Ec94617f7')) + ), + '0x1540dfad7755b40be0768c6aa0a5096fbf0215e0e8cf354dd928a178346466' + ) + }) + + it('verifyVerkleProof(): should verify verkle proofs', () => { + // Src: Kaustinen6 testnet, block 71 state root (parent of block 72) + const prestateRoot = hexToBytes( + '0x64e1a647f42e5c2e3c434531ccf529e1b3e93363a40db9fc8eec81f492123510' + ) + const executionWitness = verkleBlockJSON.executionWitness as VerkleExecutionWitness + assert.isTrue(verifyVerkleProof(verkle, prestateRoot, executionWitness)) + }) + + it('verifyVerkleProof(): should return false for invalid verkle proofs', () => { + // Random preStateRoot + const prestateRoot = randomBytes(32) + const executionWitness = verkleBlockJSON.executionWitness as VerkleExecutionWitness + // Modify the proof to make it invalid + assert.isFalse(verifyVerkleProof(verkle, prestateRoot, executionWitness)) + }) +}) + +describe('should generate valid tree keys', () => { + it('should generate valid keys for each VerkleLeafType', () => { + const stem = hexToBytes('0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d') + for (const leaf of [ + VerkleLeafType.Version, + VerkleLeafType.Balance, + VerkleLeafType.Nonce, + VerkleLeafType.CodeHash, + VerkleLeafType.CodeSize, + ]) { + const key = getVerkleKey(stem, leaf) + assert.equal(key.length, 32) + assert.deepEqual(key, concatBytes(stem, intToBytes(leaf))) + } + }) +}) diff --git a/packages/verkle/package.json b/packages/verkle/package.json index cafbf05fa0..c5797e3bae 100644 --- a/packages/verkle/package.json +++ b/packages/verkle/package.json @@ -52,8 +52,9 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { + "debug": "^4.3.4", "lru-cache": "10.1.0", - "verkle-cryptography-wasm": "^0.4.2", + "verkle-cryptography-wasm": "^0.4.5", "@ethereumjs/block": "^5.2.0", "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/util": "^9.0.3" diff --git a/packages/verkle/src/index.ts b/packages/verkle/src/index.ts index 38e111ce90..d7fde388d7 100644 --- a/packages/verkle/src/index.ts +++ b/packages/verkle/src/index.ts @@ -2,5 +2,4 @@ export * from './db/index.js' export * from './node/index.js' export * from './types.js' export * from './util/index.js' -export * from './util/keys.js' export * from './verkleTree.js' diff --git a/packages/verkle/src/node/baseVerkleNode.ts b/packages/verkle/src/node/baseVerkleNode.ts index 43edb15007..85b9a5332a 100644 --- a/packages/verkle/src/node/baseVerkleNode.ts +++ b/packages/verkle/src/node/baseVerkleNode.ts @@ -2,24 +2,22 @@ import { RLP } from '@ethereumjs/rlp' import { type VerkleNodeInterface, type VerkleNodeOptions, type VerkleNodeType } from './types.js' +import type { VerkleCrypto } from '@ethereumjs/util' + export abstract class BaseVerkleNode implements VerkleNodeInterface { public commitment: Uint8Array - public depth: number - + protected verkleCrypto: VerkleCrypto constructor(options: VerkleNodeOptions[T]) { this.commitment = options.commitment - this.depth = options.depth + this.verkleCrypto = options.verkleCrypto } - abstract commit(): Uint8Array - // Hash returns the field representation of the commitment. hash(): Uint8Array { - throw new Error('Not implemented') + return this.verkleCrypto.hashCommitment(this.commitment) } - abstract insert(key: Uint8Array, value: Uint8Array, nodeResolverFn: () => void): void - + // Returns an array of Uint8Arrays containing the values necessary to reconstruct a node from the DB (where we store them in a RLP serialized format) abstract raw(): Uint8Array[] /** diff --git a/packages/verkle/src/node/internalNode.ts b/packages/verkle/src/node/internalNode.ts index 06869e1172..21d95fdc92 100644 --- a/packages/verkle/src/node/internalNode.ts +++ b/packages/verkle/src/node/internalNode.ts @@ -1,119 +1,89 @@ -import { equalsBytes } from '@ethereumjs/util' - -import { POINT_IDENTITY } from '../util/crypto.js' +import { type VerkleCrypto } from '@ethereumjs/util' import { BaseVerkleNode } from './baseVerkleNode.js' -import { LeafNode } from './leafNode.js' import { NODE_WIDTH, VerkleNodeType } from './types.js' -import type { VerkleNode, VerkleNodeOptions } from './types.js' +import type { ChildNode, VerkleNodeOptions } from './types.js' export class InternalNode extends BaseVerkleNode { - // Array of references to children nodes - public children: Array - public copyOnWrite: Record + // Array of tuples of uncompressed commitments (i.e. 64 byte Uint8Arrays) to child nodes along with the path to that child (i.e. the partial stem) + public children: Array public type = VerkleNodeType.Internal - /* TODO: options.children is not actually used here */ constructor(options: VerkleNodeOptions[VerkleNodeType.Internal]) { super(options) - this.children = options.children ?? new Array(NODE_WIDTH).fill(null) - this.copyOnWrite = options.copyOnWrite ?? {} - } - - commit(): Uint8Array { - throw new Error('Not implemented') - } - - cowChild(_: number): void { - // Not implemented yet + this.children = + options.children ?? + new Array(256).fill({ + commitment: options.verkleCrypto.zeroCommitment, + path: new Uint8Array(), + }) } - setChild(index: number, child: VerkleNode) { - this.children[index] = child + // Updates the commitment value for a child node at the corresponding index + setChild(childIndex: number, child: ChildNode) { + // Get previous child commitment at `index` + const oldChildReference = this.children[childIndex] + // Updates the commitment to the child node at `index` + this.children[childIndex] = { ...child } + // Updates the overall node commitment based on the update to this child + this.commitment = this.verkleCrypto.updateCommitment( + this.commitment, + childIndex, + // The hashed child commitments are used when updating the internal node commitment + this.verkleCrypto.hashCommitment(oldChildReference.commitment), + this.verkleCrypto.hashCommitment(child.commitment) + ) } - static fromRawNode(rawNode: Uint8Array[], depth: number): InternalNode { + static fromRawNode(rawNode: Uint8Array[], verkleCrypto: VerkleCrypto): InternalNode { const nodeType = rawNode[0][0] if (nodeType !== VerkleNodeType.Internal) { throw new Error('Invalid node type') } - // The length of the rawNode should be the # of children, + 2 for the node type and the commitment - if (rawNode.length !== NODE_WIDTH + 2) { + // The length of the rawNode should be the # of children * 2 (for commitments and paths) + 2 for the node type and the commitment + if (rawNode.length !== NODE_WIDTH * 2 + 2) { throw new Error('Invalid node length') } - // TODO: Generate Point from rawNode value const commitment = rawNode[rawNode.length - 1] + const childrenCommitments = rawNode.slice(1, NODE_WIDTH + 1) + const childrenPaths = rawNode.slice(NODE_WIDTH + 1, NODE_WIDTH * 2 + 1) - return new InternalNode({ commitment, depth }) + const children = childrenCommitments.map((commitment, idx) => { + return { commitment, path: childrenPaths[idx] } + }) + return new InternalNode({ commitment, verkleCrypto, children }) } - static create(depth: number): InternalNode { + /** + * Generates a new Internal node with default commitment + */ + static create(verkleCrypto: VerkleCrypto): InternalNode { const node = new InternalNode({ - commitment: POINT_IDENTITY, - depth, + commitment: verkleCrypto.zeroCommitment, + verkleCrypto, }) return node } - getChildren(index: number): VerkleNode | null { - return this.children?.[index] ?? null - } - - insert(key: Uint8Array, value: Uint8Array, resolver: () => void): void { - const values = new Array(NODE_WIDTH) - values[key[31]] = value - this.insertStem(key.slice(0, 31), values, resolver) - } - - insertStem(stem: Uint8Array, values: Uint8Array[], resolver: () => void): void { - // Index of the child pointed by the next byte in the key - const childIndex = stem[this.depth] - - const child = this.children[childIndex] - - if (child instanceof LeafNode) { - this.cowChild(childIndex) - if (equalsBytes(child.stem, stem)) { - return child.insertMultiple(stem, values) - } - - // A new branch node has to be inserted. Depending - // on the next byte in both keys, a recursion into - // the moved leaf node can occur. - const nextByteInExistingKey = child.stem[this.depth + 1] - const newBranch = InternalNode.create(this.depth + 1) - newBranch.cowChild(nextByteInExistingKey) - this.children[childIndex] = newBranch - newBranch.children[nextByteInExistingKey] = child - child.depth += 1 - - const nextByteInInsertedKey = stem[this.depth + 1] - if (nextByteInInsertedKey === nextByteInExistingKey) { - return newBranch.insertStem(stem, values, resolver) - } - - // Next word differs, so this was the last level. - // Insert it directly into its final slot. - const leafNode = LeafNode.create(stem, values) - - leafNode.setDepth(this.depth + 2) - newBranch.cowChild(nextByteInInsertedKey) - newBranch.children[nextByteInInsertedKey] = leafNode - } else if (child instanceof InternalNode) { - this.cowChild(childIndex) - return child.insertStem(stem, values, resolver) - } else { - throw new Error('Invalid node type') - } + /** + * + * @param index The index in the children array to retrieve the child node commitment from + * @returns the uncompressed 64byte commitment for the child node at the `index` position in the children array + */ + getChildren(index: number): ChildNode | null { + return this.children[index] } - // TODO: go-verkle also adds the bitlist to the raw format. raw(): Uint8Array[] { - throw new Error('not implemented yet') - // return [new Uint8Array([VerkleNodeType.Internal]), ...this.children, this.commitment] + return [ + new Uint8Array([VerkleNodeType.Internal]), + ...this.children.map((child) => child.commitment), + ...this.children.map((child) => child.path), + this.commitment, + ] } } diff --git a/packages/verkle/src/node/leafNode.ts b/packages/verkle/src/node/leafNode.ts index 330fbf17c6..33e0aba77e 100644 --- a/packages/verkle/src/node/leafNode.ts +++ b/packages/verkle/src/node/leafNode.ts @@ -1,15 +1,17 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ +import { equalsBytes, intToBytes, setLengthLeft, setLengthRight } from '@ethereumjs/util' + import { BaseVerkleNode } from './baseVerkleNode.js' import { NODE_WIDTH, VerkleNodeType } from './types.js' +import { createCValues } from './util.js' -import type { Point } from '../types.js' import type { VerkleNodeOptions } from './types.js' +import type { VerkleCrypto } from '@ethereumjs/util' export class LeafNode extends BaseVerkleNode { public stem: Uint8Array - public values: Uint8Array[] - public c1: Point - public c2: Point + public values: Uint8Array[] // Array of 256 possible values represented as 32 byte Uint8Arrays + public c1?: Uint8Array + public c2?: Uint8Array public type = VerkleNodeType.Leaf constructor(options: VerkleNodeOptions[VerkleNodeType.Leaf]) { @@ -21,11 +23,76 @@ export class LeafNode extends BaseVerkleNode { this.c2 = options.c2 } - static create(stem: Uint8Array, values: Uint8Array[]): LeafNode { - throw new Error('Not implemented') + /** + * Create a new leaf node from a stem and values + * @param stem the 31 byte stem corresponding to the where the leaf node should be placed in the trie + * @param values the 256 element array of 32 byte values stored in the leaf node + * @param verkleCrypto the verkle cryptography interface + * @returns an instantiated leaf node with commitments defined + */ + static async create( + stem: Uint8Array, + values: Uint8Array[], + verkleCrypto: VerkleCrypto + ): Promise { + // Generate the value arrays for c1 and c2 + const c1Values = createCValues(values.slice(0, 128)) + const c2Values = createCValues(values.slice(128)) + let c1 = verkleCrypto.zeroCommitment + let c2 = verkleCrypto.zeroCommitment + + // Update the c1/c2 commitments for any values that are nonzero + for (let x = 0; x < 256; x++) { + if (!equalsBytes(c1Values[x], new Uint8Array(32))) { + c1 = verkleCrypto.updateCommitment(c1, x, new Uint8Array(32), c1Values[x]) + } + if (!equalsBytes(c2Values[x], new Uint8Array(32))) { + c2 = verkleCrypto.updateCommitment(c2, x, new Uint8Array(32), c2Values[x]) + } + } + + // Generate a commitment for the new leaf node, using the zero commitment as a base + // 1) Update commitment with Leaf marker (1) in position 0 + // 2) Update commitment with stem (in little endian format) in position 1 + // 3) Update commitment with c1 + // 4) update commitment with c2 + let commitment = verkleCrypto.updateCommitment( + verkleCrypto.zeroCommitment, + 0, + new Uint8Array(32), + setLengthLeft(intToBytes(1), 32) + ) + commitment = verkleCrypto.updateCommitment( + commitment, + 1, + new Uint8Array(32), + setLengthRight(stem, 32) + ) + commitment = verkleCrypto.updateCommitment( + commitment, + 2, + new Uint8Array(32), + // We hash the commitment when using in the leaf node commitment since c1 is 64 bytes long + // and we need a 32 byte input for the scalar value in `updateCommitment` + verkleCrypto.hashCommitment(c1) + ) + commitment = verkleCrypto.updateCommitment( + commitment, + 3, + new Uint8Array(32), + verkleCrypto.hashCommitment(c2) + ) + return new LeafNode({ + stem, + values, + commitment, + c1, + c2, + verkleCrypto, + }) } - static fromRawNode(rawNode: Uint8Array[], depth: number): LeafNode { + static fromRawNode(rawNode: Uint8Array[], verkleCrypto: VerkleCrypto): LeafNode { const nodeType = rawNode[0][0] if (nodeType !== VerkleNodeType.Leaf) { throw new Error('Invalid node type') @@ -37,49 +104,78 @@ export class LeafNode extends BaseVerkleNode { } const stem = rawNode[1] - // TODO: Convert the rawNode commitments to points const commitment = rawNode[2] - const c1 = rawNode[3] as unknown as Point - const c2 = rawNode[4] as unknown as Point + const c1 = rawNode[3] + const c2 = rawNode[4] const values = rawNode.slice(5, rawNode.length) - return new LeafNode({ depth, stem, values, c1, c2, commitment }) - } - commit(): Uint8Array { - throw new Error('Not implemented') + return new LeafNode({ stem, values, c1, c2, commitment, verkleCrypto }) } - getValue(index: number): Uint8Array | null { + // Retrieve the value at the provided index from the values array + getValue(index: number): Uint8Array | undefined { return this.values?.[index] ?? null } - insert(key: Uint8Array, value: Uint8Array, nodeResolverFn: () => void): void { - const values = new Array(NODE_WIDTH) - values[key[31]] = value - this.insertStem(key.slice(0, 31), values, nodeResolverFn) - } - - insertMultiple(key: Uint8Array, values: Uint8Array[]): void { - throw new Error('Not implemented') - } - - insertStem(key: Uint8Array, value: Uint8Array[], resolver: () => void): void { - throw new Error('Not implemented') + // Set the value at the provided index from the values array and update the node commitments + // TODO: Decide whether we need a separate "deleteValue" function since it has special handling + // since we never actually delete a node in a verkle trie but overwrite instead + setValue(index: number, value: Uint8Array): void { + // First we update c1 or c2 (depending on whether the index is < 128 or not) + // Generate the 16 byte values representing the 32 byte values in the half of the values array that + // contain the old value for the leaf node + const cValues = + index < 128 ? createCValues(this.values.slice(0, 128)) : createCValues(this.values.slice(128)) + // The commitment index is the 2 * the suffix (i.e. the position of the value in the values array) + // here because each 32 byte value in the leaf node is represented as two 16 byte values in the + // cValues array. + const commitmentIndex = index < 128 ? index * 2 : (index - 128) * 2 + let cCommitment = index < 128 ? this.c1 : this.c2 + // Update the commitment for the first 16 bytes of the value + cCommitment = this.verkleCrypto.updateCommitment( + cCommitment!, + commitmentIndex, + cValues[commitmentIndex], + // Right pad the value with zeroes since commitments require 32 byte scalars + setLengthRight(value.slice(0, 16), 32) + ) + // Update the commitment for the second 16 bytes of the value + cCommitment = this.verkleCrypto.updateCommitment( + cCommitment!, + commitmentIndex + 1, + cValues[commitmentIndex + 1], + // Right pad the value with zeroes since commitments require 32 byte scalars + setLengthRight(value.slice(16), 32) + ) + // Update the cCommitment corresponding to the index + let oldCCommitment: Uint8Array | undefined + if (index < 128) { + oldCCommitment = this.c1 + this.c1 = cCommitment + } else { + oldCCommitment + this.c2 = cCommitment + } + // Set the new values in the values array + this.values[index] = value + // Update leaf node commitment + const cIndex = index < 128 ? 2 : 3 + this.commitment = this.verkleCrypto.updateCommitment( + this.commitment, + cIndex, + this.verkleCrypto.hashCommitment(oldCCommitment!), + this.verkleCrypto.hashCommitment(cCommitment) + ) } - // TODO: go-verkle also adds the bitlist to the raw format. raw(): Uint8Array[] { return [ new Uint8Array([VerkleNodeType.Leaf]), this.stem, this.commitment, - this.c1.bytes(), - this.c2.bytes(), + this.c1 ?? new Uint8Array(), + this.c2 ?? new Uint8Array(), ...this.values, ] } - - setDepth(depth: number): void { - this.depth = depth - } } diff --git a/packages/verkle/src/node/types.ts b/packages/verkle/src/node/types.ts index 4070d1e7ad..e42bf114f6 100644 --- a/packages/verkle/src/node/types.ts +++ b/packages/verkle/src/node/types.ts @@ -1,12 +1,16 @@ -import type { Point } from '../types.js' import type { InternalNode } from './internalNode.js' import type { LeafNode } from './leafNode.js' +import type { VerkleCrypto } from '@ethereumjs/util' export enum VerkleNodeType { Internal, Leaf, } +export interface ChildNode { + commitment: Uint8Array // 64 byte commitment to child node + path: Uint8Array // path/partial stem to child node (used as DB key) +} export interface TypedVerkleNode { [VerkleNodeType.Internal]: InternalNode [VerkleNodeType.Leaf]: LeafNode @@ -15,30 +19,24 @@ export interface TypedVerkleNode { export type VerkleNode = TypedVerkleNode[VerkleNodeType] export interface VerkleNodeInterface { - commit(): Uint8Array - hash(): any + hash(): Uint8Array serialize(): Uint8Array } interface BaseVerkleNodeOptions { - // Value of the commitment commitment: Uint8Array - depth: number + verkleCrypto: VerkleCrypto } interface VerkleInternalNodeOptions extends BaseVerkleNodeOptions { // Children nodes of this internal node. - children?: VerkleNode[] - - // Values of the child commitments before the tree is modified by inserts. - // This is useful because the delta of the child commitments can be used to efficiently update the node's commitment - copyOnWrite?: Record + children?: ChildNode[] } interface VerkleLeafNodeOptions extends BaseVerkleNodeOptions { stem: Uint8Array values: Uint8Array[] - c1: Point - c2: Point + c1?: Uint8Array + c2?: Uint8Array } export interface VerkleNodeOptions { diff --git a/packages/verkle/src/node/util.ts b/packages/verkle/src/node/util.ts index 16726076dc..e160ead71d 100644 --- a/packages/verkle/src/node/util.ts +++ b/packages/verkle/src/node/util.ts @@ -1,30 +1,59 @@ import { RLP } from '@ethereumjs/rlp' +import { bigIntToBytes, bytesToBigInt, setLengthRight } from '@ethereumjs/util' import { InternalNode } from './internalNode.js' import { LeafNode } from './leafNode.js' import { type VerkleNode, VerkleNodeType } from './types.js' -export function decodeRawNode(raw: Uint8Array[]): VerkleNode { +import type { VerkleCrypto } from '@ethereumjs/util' + +export function decodeRawNode(raw: Uint8Array[], verkleCrypto: VerkleCrypto): VerkleNode { const nodeType = raw[0][0] - const depth = 0 switch (nodeType) { case VerkleNodeType.Internal: - return InternalNode.fromRawNode(raw, depth) + return InternalNode.fromRawNode(raw, verkleCrypto) case VerkleNodeType.Leaf: - return LeafNode.fromRawNode(raw, depth) + return LeafNode.fromRawNode(raw, verkleCrypto) default: throw new Error('Invalid node type') } } -export function decodeNode(raw: Uint8Array) { +export function decodeNode(raw: Uint8Array, verkleCrypto: VerkleCrypto) { const decoded = RLP.decode(Uint8Array.from(raw)) as Uint8Array[] if (!Array.isArray(decoded)) { throw new Error('Invalid node') } - return decodeRawNode(decoded) + return decodeRawNode(decoded, verkleCrypto) } export function isRawNode(node: Uint8Array | Uint8Array[]): node is Uint8Array[] { return Array.isArray(node) && !(node instanceof Uint8Array) } + +/*** + * Converts 128 32byte values of a leaf node into 16 byte values for generating a commitment for half of a + * leaf node's values + * @param values - an array of Uint8Arrays representing the first or second set of 128 values stored by the verkle trie leaf node + * @param deletedValues - an array of booleans where a value of true at a given position indicates a value + * that is being deleted - should always be false if generating C2 values + * Returns an array of 256 16byte UintArrays with the leaf marker set for each value that is deleted + */ +export const createCValues = (values: Uint8Array[], deletedValues = new Array(128).fill(false)) => { + if (values.length !== 128 || deletedValues.length !== 128) + throw new Error(`got wrong number of values, expected 128, got ${values.length}`) + const expandedValues: Uint8Array[] = new Array(256) + for (let x = 0; x < 128; x++) { + // We add 16 trailing zeros to each value since all commitments are padded to an array of 32 byte values + expandedValues[x * 2] = setLengthRight( + deletedValues[x] === true + ? // TODO: Improve performance by only flipping the 129th bit of `expandedValues[x]` (instead of bigint addition) + bigIntToBytes(bytesToBigInt(values[x].subarray(0, 16)) + BigInt(2 ** 128)) + : values[x].slice(0, 16), + 32 + ) + // TODO: Decide if we should use slice or subarray here (i.e. do we need to copy these slices or not) + expandedValues[x * 2 + 1] = setLengthRight(values[x].slice(16), 32) + } + return expandedValues +} diff --git a/packages/verkle/src/types.ts b/packages/verkle/src/types.ts index 81614ae5a0..06c09e2024 100644 --- a/packages/verkle/src/types.ts +++ b/packages/verkle/src/types.ts @@ -1,64 +1,10 @@ -import { intToBytes, utf8ToBytes } from '@ethereumjs/util' +import { utf8ToBytes } from '@ethereumjs/util' -import type { VerkleNode } from './node/index.js' -import type { WalkController } from './util/walkController.js' import type { DB } from '@ethereumjs/util' -import type { VerkleCrypto as VerkleFFI } from 'verkle-cryptography-wasm' // Field representation of a commitment export interface Fr {} -// Elliptic curve point representation of a commitment -export interface Point { - // Bytes returns the compressed serialized version of the element. - bytes(): Uint8Array - // BytesUncompressed returns the uncompressed serialized version of the element. - bytesUncompressed(): Uint8Array - - // SetBytes deserializes a compressed group element from buf. - // This method does all the proper checks assuming the bytes come from an - // untrusted source. - setBytes(bytes: Uint8Array): void - - // SetBytesUncompressed deserializes an uncompressed group element from buf. - setBytesUncompressed(bytes: Uint8Array, trusted: boolean): void - - // computes X/Y - mapToBaseField(): Point - - // mapToScalarField maps a group element to the scalar field. - mapToScalarField(field: Fr): void - - // Equal returns true if p and other represent the same point. - equal(secondPoint: Point): boolean - - // SetIdentity sets p to the identity element. - setIdentity(): Point - - // Double sets p to 2*p1. - double(point1: Point): Point - - // Add sets p to p1+p2. - add(point1: Point, point2: Point): Point - - // Sub sets p to p1-p2. - sub(point1: Point, point2: Point): Point - - // IsOnCurve returns true if p is on the curve. - isOnCurve(): boolean - - normalise(): void - - // Set sets p to p1. - set(): Point - - // Neg sets p to -p1. - neg(): Point - - // ScalarMul sets p to p1*s. - scalarMul(point1: Point, scalarMont: Fr): Point -} - export type Proof = Uint8Array[] export interface VerkleTreeOpts { @@ -69,7 +15,7 @@ export interface VerkleTreeOpts { /** * A database instance. */ - db?: DB + db: DB /** * A `Uint8Array` for the root of a previously stored tree @@ -113,32 +59,4 @@ export type Checkpoint = { root: Uint8Array } -export type FoundNodeFunction = ( - nodeRef: Uint8Array, - node: VerkleNode | null, - key: Uint8Array, - walkController: WalkController -) => void - export const ROOT_DB_KEY = utf8ToBytes('__root__') - -export type VerkleCrypto = VerkleFFI - -export enum LeafType { - Version = 0, - Balance = 1, - Nonce = 2, - CodeHash = 3, - CodeSize = 4, -} - -export const VERSION_LEAF_KEY = intToBytes(LeafType.Version) -export const BALANCE_LEAF_KEY = intToBytes(LeafType.Balance) -export const NONCE_LEAF_KEY = intToBytes(LeafType.Nonce) -export const CODE_HASH_LEAF_KEY = intToBytes(LeafType.CodeHash) -export const CODE_SIZE_LEAF_KEY = intToBytes(LeafType.CodeSize) - -export const HEADER_STORAGE_OFFSET = 64 -export const CODE_OFFSET = 128 -export const VERKLE_NODE_WIDTH = 256 -export const MAIN_STORAGE_OFFSET = BigInt(256) ** BigInt(31) diff --git a/packages/verkle/src/util/bytes.ts b/packages/verkle/src/util/bytes.ts index 9a5b83da93..a3d1bc5938 100644 --- a/packages/verkle/src/util/bytes.ts +++ b/packages/verkle/src/util/bytes.ts @@ -8,15 +8,29 @@ */ export function matchingBytesLength(bytes1: Uint8Array, bytes2: Uint8Array): number { let count = 0 - - // The minimum length of both arrays const minLength = Math.min(bytes1.length, bytes2.length) - for (let i = 0; i < minLength; i++) { + // Unroll the loop for better performance + for (let i = 0; i < minLength - 3; i += 4) { + // Compare 4 bytes at a time + if ( + bytes1[i] === bytes2[i] && + bytes1[i + 1] === bytes2[i + 1] && + bytes1[i + 2] === bytes2[i + 2] && + bytes1[i + 3] === bytes2[i + 3] + ) { + count += 4 + } else { + // Break early if a mismatch is found + break + } + } + + // Handle any remaining elements + for (let i = minLength - (minLength % 4); i < minLength; i++) { if (bytes1[i] === bytes2[i]) { count++ } else { - // Stop counting as soon as a mismatch is found break } } diff --git a/packages/verkle/src/util/crypto.ts b/packages/verkle/src/util/crypto.ts deleted file mode 100644 index 5ba3310159..0000000000 --- a/packages/verkle/src/util/crypto.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - type Address, - bigIntToBytes, - bytesToHex, - int32ToBytes, - setLengthLeft, - setLengthRight, -} from '@ethereumjs/util' - -import type { VerkleExecutionWitness } from '@ethereumjs/block' -import type { VerkleCrypto } from 'verkle-cryptography-wasm' - -/** - * @dev Returns the 31-bytes verkle tree stem for a given address and tree index. - * @dev Assumes that the verkle node width = 256 - * @param ffi The verkle ffi object from verkle-crypotography-wasm. - * @param address The address to generate the tree key for. - * @param treeIndex The index of the tree to generate the key for. Defaults to 0. - * @return The 31-bytes verkle tree stem as a Uint8Array. - */ -export function getStem( - ffi: VerkleCrypto, - address: Address, - treeIndex: number | bigint = 0 -): Uint8Array { - const address32 = setLengthLeft(address.toBytes(), 32) - - let treeIndexBytes: Uint8Array - if (typeof treeIndex === 'number') { - treeIndexBytes = setLengthRight(int32ToBytes(Number(treeIndex), true), 32) - } else { - treeIndexBytes = setLengthRight(bigIntToBytes(BigInt(treeIndex), true).slice(0, 32), 32) - } - - const treeStem = ffi.getTreeKey(address32, treeIndexBytes, 0).slice(0, 31) - - return treeStem -} - -/** - * Verifies that the executionWitness is valid for the given prestateRoot. - * @param ffi The verkle ffi object from verkle-crypotography-wasm. - * @param prestateRoot The prestateRoot matching the executionWitness. - * @param executionWitness The verkle execution witness. - * @returns {boolean} Whether or not the executionWitness belongs to the prestateRoot. - */ -export function verifyProof( - ffi: VerkleCrypto, - prestateRoot: Uint8Array, - executionWitness: VerkleExecutionWitness -): boolean { - return ffi.verifyExecutionWitnessPreState( - bytesToHex(prestateRoot), - JSON.stringify(executionWitness) - ) -} - -export const POINT_IDENTITY = new Uint8Array(0) diff --git a/packages/verkle/src/util/index.ts b/packages/verkle/src/util/index.ts index 972e26b441..2da7bbb112 100644 --- a/packages/verkle/src/util/index.ts +++ b/packages/verkle/src/util/index.ts @@ -1,4 +1,2 @@ export * from './bytes.js' -export * from './crypto.js' export * from './tasks.js' -export * from './walkController.js' diff --git a/packages/verkle/src/util/keys.ts b/packages/verkle/src/util/keys.ts deleted file mode 100644 index 5f2a3adb7f..0000000000 --- a/packages/verkle/src/util/keys.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { concatBytes, setLengthRight, toBytes } from '@ethereumjs/util' - -import { - BALANCE_LEAF_KEY, - CODE_HASH_LEAF_KEY, - CODE_OFFSET, - CODE_SIZE_LEAF_KEY, - HEADER_STORAGE_OFFSET, - LeafType, - MAIN_STORAGE_OFFSET, - NONCE_LEAF_KEY, - VERKLE_NODE_WIDTH, - VERSION_LEAF_KEY, -} from '../types.js' - -import { getStem } from './crypto.js' - -import type { VerkleCrypto } from '../types.js' -import type { Address } from '@ethereumjs/util' - -/** - * @dev Returns the tree key for a given verkle tree stem, and sub index. - * @dev Assumes that the verkle node width = 256 - * @param stem The 31-bytes verkle tree stem as a Uint8Array. - * @param subIndex The sub index of the tree to generate the key for as a Uint8Array. - * @return The tree key as a Uint8Array. - */ - -export const getKey = (stem: Uint8Array, leaf: LeafType | Uint8Array) => { - switch (leaf) { - case LeafType.Version: - return concatBytes(stem, VERSION_LEAF_KEY) - case LeafType.Balance: - return concatBytes(stem, BALANCE_LEAF_KEY) - case LeafType.Nonce: - return concatBytes(stem, NONCE_LEAF_KEY) - case LeafType.CodeHash: - return concatBytes(stem, CODE_HASH_LEAF_KEY) - case LeafType.CodeSize: - return concatBytes(stem, CODE_SIZE_LEAF_KEY) - default: - return concatBytes(stem, leaf) - } -} - -export function getTreeIndexesForStorageSlot(storageKey: bigint): { - treeIndex: bigint - subIndex: number -} { - let position: bigint - if (storageKey < CODE_OFFSET - HEADER_STORAGE_OFFSET) { - position = BigInt(HEADER_STORAGE_OFFSET) + storageKey - } else { - position = MAIN_STORAGE_OFFSET + storageKey - } - - const treeIndex = position / BigInt(VERKLE_NODE_WIDTH) - const subIndex = Number(position % BigInt(VERKLE_NODE_WIDTH)) - - return { treeIndex, subIndex } -} - -export function getTreeIndicesForCodeChunk(chunkId: number) { - const treeIndex = Math.floor((CODE_OFFSET + chunkId) / VERKLE_NODE_WIDTH) - const subIndex = (CODE_OFFSET + chunkId) % VERKLE_NODE_WIDTH - return { treeIndex, subIndex } -} - -export const getTreeKeyForCodeChunk = async ( - address: Address, - chunkId: number, - verkleCrypto: VerkleCrypto -) => { - const { treeIndex, subIndex } = getTreeIndicesForCodeChunk(chunkId) - return concatBytes(getStem(verkleCrypto, address, treeIndex), toBytes(subIndex)) -} - -export const chunkifyCode = (code: Uint8Array) => { - // Pad code to multiple of 31 bytes - if (code.length % 31 !== 0) { - const paddingLength = 31 - (code.length % 31) - code = setLengthRight(code, code.length + paddingLength) - } - - throw new Error('Not implemented') -} - -export const getTreeKeyForStorageSlot = async ( - address: Address, - storageKey: bigint, - verkleCrypto: VerkleCrypto -) => { - const { treeIndex, subIndex } = getTreeIndexesForStorageSlot(storageKey) - - return concatBytes(getStem(verkleCrypto, address, treeIndex), toBytes(subIndex)) -} diff --git a/packages/verkle/src/util/walkController.ts b/packages/verkle/src/util/walkController.ts deleted file mode 100644 index 18333080fd..0000000000 --- a/packages/verkle/src/util/walkController.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { InternalNode, LeafNode } from '../node/index.js' - -import { PrioritizedTaskExecutor } from './tasks.js' - -import type { VerkleNode } from '../node/types.js' -import type { FoundNodeFunction } from '../types.js' -import type { VerkleTree } from '../verkleTree.js' - -/** - * WalkController is an interface to control how the tree is being traversed. - */ -export class WalkController { - readonly onNode: FoundNodeFunction - readonly taskExecutor: PrioritizedTaskExecutor - readonly tree: VerkleTree - private resolve: Function - private reject: Function - - /** - * Creates a new WalkController - * @param onNode - The `FoundNodeFunction` to call if a node is found. - * @param tree - The `VerkleTree` to walk on. - * @param poolSize - The size of the task queue. - */ - private constructor(onNode: FoundNodeFunction, tree: VerkleTree, poolSize: number) { - this.onNode = onNode - this.taskExecutor = new PrioritizedTaskExecutor(poolSize) - this.tree = tree - this.resolve = () => {} - this.reject = () => {} - } - - /** - * Async function to create and start a new walk over a tree. - * @param onNode - The `FoundNodeFunction to call if a node is found. - * @param tree - The tree to walk on. - * @param root - The root key to walk on. - * @param poolSize - Task execution pool size to prevent OOM errors. Defaults to 500. - */ - static async newWalk( - onNode: FoundNodeFunction, - tree: VerkleTree, - root: Uint8Array, - poolSize?: number - ): Promise { - const strategy = new WalkController(onNode, tree, poolSize ?? 500) - await strategy.startWalk(root) - } - - private async startWalk(root: Uint8Array): Promise { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - this.resolve = resolve - this.reject = reject - let node - try { - node = await this.tree.lookupNode(root) - } catch (error) { - return this.reject(error) - } - this.processNode(root, node, new Uint8Array(0)) - }) - } - - /** - * Run all children of a node. Priority of these nodes are the key length of the children. - * @param node - Node to retrieve all children from of and call onNode on. - * @param key - The current `key` which would yield the `node` when trying to get this node with a `get` operation. - */ - allChildren(node: VerkleNode, key: Uint8Array = new Uint8Array()) { - if (node instanceof LeafNode) { - return - } - - const children = node.children.map((nodeRef, index) => ({ - keyExtension: index, - nodeRef, - })) - - for (const child of children) { - if (child.nodeRef !== null) { - const childKey = new Uint8Array([...key, child.keyExtension]) - this.pushNodeToQueue(child.nodeRef.hash(), childKey) - } - } - } - - /** - * Push a node to the queue. If the queue has places left for tasks, the node is executed immediately, otherwise it is queued. - * @param nodeRef - Push a node reference to the event queue. This reference is a 32-byte keccak hash of the value corresponding to the `key`. - * @param key - The current key. - * @param priority - Optional priority, defaults to key length - */ - pushNodeToQueue(nodeRef: Uint8Array, key: Uint8Array = new Uint8Array(0), priority?: number) { - this.taskExecutor.executeOrQueue( - priority ?? key.length, - async (taskFinishedCallback: Function) => { - let childNode - try { - childNode = await this.tree.lookupNode(nodeRef) - } catch (error: any) { - return this.reject(error) - } - taskFinishedCallback() // this marks the current task as finished. If there are any tasks left in the queue, this will immediately execute the first task. - this.processNode(nodeRef, childNode as VerkleNode, key) - } - ) - } - - /** - * Push the child of an internal node to the event queue. - * @param node - The node to select a children from. Should be an InternalNode. - * @param key - The current key which leads to the corresponding node. - * @param childIndex - The child index to add to the event queue. - * @param priority - Optional priority of the event, defaults to the total key length. - */ - pushChildrenAtIndex( - node: InternalNode, - key: Uint8Array = new Uint8Array(0), - childIndex: number, - priority?: number - ) { - if (!(node instanceof InternalNode)) { - throw new Error('Expected internal node') - } - const childRef = node.getChildren(childIndex) - if (!childRef) { - throw new Error('Could not get node at childIndex') - } - const childKey = new Uint8Array([...key, childIndex]) - this.pushNodeToQueue(childRef.hash(), childKey, priority ?? childKey.length) - } - - private processNode( - nodeRef: Uint8Array, - node: VerkleNode | null, - key: Uint8Array = new Uint8Array(0) - ) { - this.onNode(nodeRef, node, key, this) - if (this.taskExecutor.finished()) { - // onNode should schedule new tasks. If no tasks was added and the queue is empty, then we have finished our walk. - this.resolve() - } - } -} diff --git a/packages/verkle/src/verkleTree.ts b/packages/verkle/src/verkleTree.ts index 261865903d..bd637f43a6 100644 --- a/packages/verkle/src/verkleTree.ts +++ b/packages/verkle/src/verkleTree.ts @@ -1,27 +1,34 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { KeyEncoding, Lock, ValueEncoding, equalsBytes, zeros } from '@ethereumjs/util' +import { + KeyEncoding, + Lock, + MapDB, + ValueEncoding, + bytesToHex, + equalsBytes, + zeros, +} from '@ethereumjs/util' +import debug from 'debug' import { loadVerkleCrypto } from 'verkle-cryptography-wasm' import { CheckpointDB } from './db/checkpoint.js' import { InternalNode } from './node/internalNode.js' import { LeafNode } from './node/leafNode.js' -import { decodeNode, decodeRawNode, isRawNode } from './node/util.js' +import { type VerkleNode } from './node/types.js' +import { decodeNode } from './node/util.js' import { type Proof, ROOT_DB_KEY, type VerkleTreeOpts, type VerkleTreeOptsWithDefaults, } from './types.js' -import { WalkController, matchingBytesLength } from './util/index.js' - -import type { VerkleNode } from './node/types.js' -import type { FoundNodeFunction } from './types.js' -import type { BatchDBOp, DB, PutBatch } from '@ethereumjs/util' +import { matchingBytesLength } from './util/index.js' +import type { DB, PutBatch, VerkleCrypto } from '@ethereumjs/util' +import type { Debugger } from 'debug' interface Path { node: VerkleNode | null remaining: Uint8Array - stack: VerkleNode[] + stack: Array<[VerkleNode, Uint8Array]> } /** @@ -32,6 +39,7 @@ export class VerkleTree { useRootPersistence: false, cacheSize: 0, verkleCrypto: undefined, + db: new MapDB(), } /** The root for an empty tree */ @@ -43,7 +51,12 @@ export class VerkleTree { protected _lock = new Lock() protected _root: Uint8Array - protected verkleCrypto: any + protected verkleCrypto: VerkleCrypto + + /** Debug logging */ + protected DEBUG: boolean + protected _debug: Debugger = debug('verkle') + protected debug: (...args: any) => void /** * Creates a new verkle tree. * @param opts Options for instantiating the verkle tree @@ -70,6 +83,25 @@ export class VerkleTree { } this.verkleCrypto = opts?.verkleCrypto + + this.DEBUG = + typeof window === 'undefined' ? process?.env?.DEBUG?.includes('ethjs') ?? false : false + this.debug = this.DEBUG + ? (message: string, namespaces: string[] = []) => { + let log = this._debug + for (const name of namespaces) { + log = log.extend(name) + } + log(message) + } + : (..._: any) => {} + + this.DEBUG && + this.debug(`Trie created: + || Root: ${bytesToHex(this._root)} + || Persistent: ${this._opts.useRootPersistence} + || CacheSize: ${this._opts.cacheSize} + || ----------------`) } static async create(opts?: VerkleTreeOpts) { @@ -89,12 +121,17 @@ export class VerkleTree { } } - if (opts === undefined) { - opts = { - verkleCrypto: loadVerkleCrypto, + if (opts?.verkleCrypto === undefined) { + const verkleCrypto = await loadVerkleCrypto() + if (opts === undefined) + opts = { + verkleCrypto, + db: new MapDB(), + } + else { + opts.verkleCrypto = verkleCrypto } } - opts.verkleCrypto = await loadVerkleCrypto() return new VerkleTree(opts) } @@ -135,7 +172,7 @@ export class VerkleTree { */ async checkRoot(root: Uint8Array): Promise { try { - const value = await this.lookupNode(root) + const value = await this._db.get(root) return value !== null } catch (error: any) { if (error.message === 'Missing node in DB') { @@ -149,58 +186,36 @@ export class VerkleTree { /** * Gets a value given a `key` * @param key - the key to search for - * @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false) - * @returns A Promise that resolves to `Uint8Array` if a value was found or `null` if no value was found. + * @returns A Promise that resolves to `Uint8Array` if a value was found or `undefined` if no value was found. */ - async get(key: Uint8Array, throwIfMissing = false): Promise { - const node = await this.findLeafNode(key, throwIfMissing) - if (node !== null) { - const keyLastByte = key[key.length - 1] - + async get(key: Uint8Array): Promise { + if (key.length !== 32) throw new Error(`expected key with length 32; got ${key.length}`) + const stem = key.slice(0, 31) + const suffix = key[key.length - 1] + this.DEBUG && this.debug(`Stem: ${bytesToHex(stem)}; Suffix: ${suffix}`, ['GET']) + const res = await this.findPath(stem) + + if (res.node instanceof LeafNode) { // The retrieved leaf node contains an array of 256 possible values. // The index of the value we want is at the key's last byte - return node.values?.[keyLastByte] ?? null + const value = res.node.getValue(suffix) + this.DEBUG && + this.debug(`Value: ${value === undefined ? 'undefined' : bytesToHex(value)}`, ['GET']) + return value } - return null + return } /** - * Stores a given `value` at the given `key` or do a delete if `value` is empty - * (delete operations are only executed on DB with `deleteFromDB` set to `true`) + * Stores a given `value` at the given `key` or do a delete if `value` is empty Uint8Array * @param key - the key to store the value at * @param value - the value to store * @returns A Promise that resolves once value is stored. */ - async put(key: Uint8Array, value: Uint8Array): Promise { - await this._db.put(key, value) - - // Find or create the leaf node - const leafNode = await this.findLeafNode(key, false) - if (leafNode === null) { - // If leafNode is missing, create it - // leafNode = LeafNode.create() - throw new Error('Not implemented') - } - - // Walk up the tree and update internal nodes - let currentNode: VerkleNode = leafNode - let currentKey = leafNode.stem - let currentDepth = leafNode.depth - - while (currentDepth > 0) { - const parentKey = currentKey.slice(0, -1) - const parentIndex = currentKey[currentKey.length - 1] - const parentNode = InternalNode.create(currentDepth) - parentNode.children[parentIndex] = currentNode - await this._db.put(parentKey, parentNode.serialize()) - - currentNode = parentNode - currentKey = parentKey - currentDepth-- - } - - this._root = currentNode.hash() + // TODO: Rewrite following logic in verkle.spec.ts "findPath validation" test + async put(_key: Uint8Array, _value: Uint8Array): Promise { + throw new Error('not implemented') } /** @@ -209,185 +224,140 @@ export class VerkleTree { * @param key - the search key * @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false) */ - async findPath(key: Uint8Array, throwIfMissing = false): Promise { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - const stack: VerkleNode[] = [] - - const onFound: FoundNodeFunction = async (_, node, keyProgress, walkController) => { - if (node === null) { - return reject(new Error('Path not found')) - } - const keyRemainder = key.slice(matchingBytesLength(keyProgress, key)) - stack.push(node) - - if (node instanceof InternalNode) { - if (keyRemainder.length === 0) { - // we exhausted the key without finding a node - resolve({ node, remaining: new Uint8Array(0), stack }) - } else { - const childrenIndex = keyRemainder[0] - const childNode = node.getChildren(childrenIndex) - if (childNode === null) { - // There are no more nodes to find and we didn't find the key - resolve({ node: null, remaining: keyRemainder, stack }) - } else { - // node found, continue search from children - walkController.pushChildrenAtIndex(node, keyProgress, childrenIndex) - } - } - } else if (node instanceof LeafNode) { - // The stem of the leaf node should be the full key minus the last byte - const stem = key.slice(0, key.length - 1) - if (equalsBytes(stem, node.stem)) { - // keys match, return node with empty key - resolve({ node, remaining: new Uint8Array(0), stack }) - } else { - // reached leaf but keys don't match - resolve({ node: null, remaining: keyRemainder, stack }) - } - } - } + async findPath(key: Uint8Array): Promise { + this.DEBUG && this.debug(`Path (${key.length}): [${bytesToHex(key)}]`, ['FIND_PATH']) + const result: Path = { + node: null, + stack: [], + remaining: key, + } + if (equalsBytes(this.root(), this.EMPTY_TREE_ROOT)) return result - // walk tree and process nodes - try { - await this.walkTree(this.root(), onFound) - } catch (error: any) { - if (error.message === 'Missing node in DB' && !throwIfMissing) { - // pass - } else { - reject(error) - } - } + // Get root node + let rawNode = await this._db.get(this.root()) + if (rawNode === undefined) + throw new Error('root node should exist when root not empty tree root') - // Resolve if walkTree finishes without finding any nodes - resolve({ node: null, remaining: new Uint8Array(0), stack }) - }) - } + const rootNode = decodeNode(rawNode, this.verkleCrypto) as InternalNode - /** - * Walks a tree until finished. - * @param root - * @param onFound - callback to call when a node is found. This schedules new tasks. If no tasks are available, the Promise resolves. - * @returns Resolves when finished walking tree. - */ - async walkTree(root: Uint8Array, onFound: FoundNodeFunction): Promise { - await WalkController.newWalk(onFound, this, root) - } + this.DEBUG && this.debug(`Starting with Root Node: [${bytesToHex(this.root())}]`, ['FIND_PATH']) + result.stack.push([rootNode, this.root()]) + let child = rootNode.children[key[0]] - /** - * Tries to find the leaf node leading up to the given key, or null if not found. - * @param key - the search key - * @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false) - */ - async findLeafNode(key: Uint8Array, throwIfMissing = false): Promise { - const { node } = await this.findPath(key, throwIfMissing) - if (!(node instanceof LeafNode)) { - if (throwIfMissing) { - throw new Error('leaf node not found') + // Root node doesn't contain a child node's commitment on the first byte of the path so we're done + if (equalsBytes(child.commitment, this.verkleCrypto.zeroCommitment)) { + this.DEBUG && this.debug(`Partial Path ${key[0]} - found no child.`, ['FIND_PATH']) + return result + } + let finished = false + while (!finished) { + // Look up child node by node hash + rawNode = await this._db.get(this.verkleCrypto.hashCommitment(child.commitment)) + // We should always find the node if the path is specified in child.path + if (rawNode === undefined) throw new Error(`missing node at ${bytesToHex(child.path)}`) + const decodedNode = decodeNode(rawNode, this.verkleCrypto) + + // Calculate the index of the last matching byte in the key + const matchingKeyLength = matchingBytesLength(key, child.path) + const foundNode = equalsBytes(key, child.path) + if (foundNode || child.path.length >= key.length || decodedNode instanceof LeafNode) { + // If the key and child.path are equal, then we found the node + // If the child.path is the same length or longer than the key but doesn't match it + // or the found node is a leaf node, we've found another node where this node should + // be if it existed in the trie + // i.e. the node doesn't exist in the trie + finished = true + if (foundNode) { + this.DEBUG && + this.debug( + `Path ${bytesToHex(key)} - found full path to node ${bytesToHex( + decodedNode.hash() + )}.`, + ['FIND_PATH'] + ) + result.node = decodedNode + result.remaining = new Uint8Array() + return result + } + // We found a different node than the one specified by `key` + // so the sought node doesn't exist + result.remaining = key.slice(matchingKeyLength) + this.DEBUG && + this.debug( + `Path ${bytesToHex( + key.slice(0, matchingKeyLength) + )} - found path to nearest node ${bytesToHex( + decodedNode.hash() + )} but target node not found.`, + ['FIND_PATH'] + ) + result.stack.push([decodedNode, key.slice(0, matchingKeyLength)]) + return result } - return null + // Push internal node to path stack + result.stack.push([decodedNode, key.slice(0, matchingKeyLength)]) + this.DEBUG && + this.debug( + `Partial Path ${bytesToHex( + key.slice(0, matchingKeyLength) + )} - found next node in path ${bytesToHex(decodedNode.hash())}.`, + ['FIND_PATH'] + ) + // Get the next child node in the path + const childIndex = key[matchingKeyLength] + child = decodedNode.children[childIndex] } - return node + this.DEBUG && + this.debug( + `Found partial path ${key.slice( + 31 - result.remaining.length + )} but sought node is not present in trie.`, + ['FIND_PATH'] + ) + return result } /** - * Creates the initial node from an empty tree. + * Create empty root node for initializing an empty tree. * @private */ - protected async _createInitialNode(key: Uint8Array, value: Uint8Array): Promise { - throw new Error('Not implemented') - } - /** - * Retrieves a node from db by hash. - */ - async lookupNode(node: Uint8Array | Uint8Array[]): Promise { - if (isRawNode(node)) { - return decodeRawNode(node) - } - const value = await this._db.get(node) - if (value !== undefined) { - return decodeNode(value) - } else { - // Dev note: this error message text is used for error checking in `checkRoot`, `verifyProof`, and `findPath` - throw new Error('Missing node in DB') - } - } + protected async _createRootNode(): Promise { + const rootNode = new InternalNode({ + commitment: this.verkleCrypto.zeroCommitment, + verkleCrypto: this.verkleCrypto, + }) - /** - * Updates a node. - * @private - * @param key - * @param value - * @param keyRemainder - * @param stack - */ - protected async _updateNode( - k: Uint8Array, - value: Uint8Array, - keyRemainder: Uint8Array, - stack: VerkleNode[] - ): Promise { - throw new Error('Not implemented') + // Update the child node's commitment and path + this.DEBUG && this.debug(`No root node. Creating new root node`, ['INITIALIZE']) + // Set trie root to serialized (aka compressed) commitment for later use in verkle proof + this.root(this.verkleCrypto.serializeCommitment(rootNode.commitment)) + await this.saveStack([[this.root(), rootNode]]) + return } /** * Saves a stack of nodes to the database. * - * @param key - the key. Should follow the stack - * @param stack - a stack of nodes to the value given by the key - * @param opStack - a stack of levelup operations to commit at the end of this function - */ - async saveStack( - key: Uint8Array, - stack: VerkleNode[], - opStack: PutBatch[] - ): Promise { - throw new Error('Not implemented') - } - - /** - * Formats node to be saved by `levelup.batch`. - * @private - * @param node - the node to format. - * @param topLevel - if the node is at the top level. - * @param opStack - the opStack to push the node's data. - * @param remove - whether to remove the node - * @returns The node's hash used as the key or the rawNode. + * @param putStack - an array of tuples of keys (the partial path of the node in the trie) and nodes (VerkleNodes) */ - _formatNode( - node: VerkleNode, - topLevel: boolean, - opStack: PutBatch, - remove: boolean = false - ): Uint8Array { - throw new Error('Not implemented') - } - /** - * The given hash of operations (key additions or deletions) are executed on the tree - * (delete operations are only executed on DB with `deleteFromDB` set to `true`) - * @example - * const ops = [ - * { type: 'del', key: Uint8Array.from('father') } - * , { type: 'put', key: Uint8Array.from('name'), value: Uint8Array.from('Yuri Irsenovich Kim') } - * , { type: 'put', key: Uint8Array.from('dob'), value: Uint8Array.from('16 February 1941') } - * , { type: 'put', key: Uint8Array.from('spouse'), value: Uint8Array.from('Kim Young-sook') } - * , { type: 'put', key: Uint8Array.from('occupation'), value: Uint8Array.from('Clown') } - * ] - * await tree.batch(ops) - * @param ops - */ - async batch(ops: BatchDBOp[]): Promise { - throw new Error('Not implemented') + async saveStack(putStack: [Uint8Array, VerkleNode][]): Promise { + const opStack = putStack.map(([key, node]) => { + return { + type: 'put', + key, + value: node.serialize(), + } as PutBatch + }) + await this._db.batch(opStack) } /** * Saves the nodes from a proof into the tree. * @param proof */ - async fromProof(proof: Proof): Promise { + async fromProof(_proof: Proof): Promise { throw new Error('Not implemented') } @@ -395,7 +365,7 @@ export class VerkleTree { * Creates a proof from a tree and key that can be verified using {@link VerkleTree.verifyProof}. * @param key */ - async createProof(key: Uint8Array): Promise { + async createProof(_key: Uint8Array): Promise { throw new Error('Not implemented') } @@ -408,9 +378,9 @@ export class VerkleTree { * @returns The value from the key, or null if valid proof of non-existence. */ async verifyProof( - rootHash: Uint8Array, - key: Uint8Array, - proof: Proof + _rootHash: Uint8Array, + _key: Uint8Array, + _proof: Proof ): Promise { throw new Error('Not implemented') } @@ -452,22 +422,13 @@ export class VerkleTree { /** * Persists the root hash in the underlying database */ + // TODO: Fix how we reference the root node in `findPath` so this method will work correctly async persistRoot() { if (this._opts.useRootPersistence) { await this._db.put(ROOT_DB_KEY, this.root()) } } - /** - * Finds all nodes that are stored directly in the db - * (some nodes are stored raw inside other nodes) - * called by {@link ScratchReadStream} - * @private - */ - protected async _findDbNodes(onFound: () => void): Promise { - throw new Error('Not implemented') - } - /** * Is the tree during a checkpoint phase? */ diff --git a/packages/verkle/test/crypto.spec.ts b/packages/verkle/test/crypto.spec.ts deleted file mode 100644 index c6f21af379..0000000000 --- a/packages/verkle/test/crypto.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Address, bytesToHex, hexToBytes, randomBytes } from '@ethereumjs/util' -import { loadVerkleCrypto } from 'verkle-cryptography-wasm' -import { assert, beforeAll, describe, it } from 'vitest' - -import * as verkleBlockJSON from '../../statemanager/test/testdata/verkleKaustinen6Block72.json' -import { getStem, verifyProof } from '../src/index.js' - -import type { VerkleCrypto } from '../src/index.js' -import type { VerkleExecutionWitness } from '@ethereumjs/block' - -describe('Verkle cryptographic helpers', () => { - let verkle: VerkleCrypto - beforeAll(async () => { - verkle = await loadVerkleCrypto() - }) - - it('getStem(): returns the expected stems', () => { - // Empty address - assert.equal( - bytesToHex(getStem(verkle, Address.fromString('0x0000000000000000000000000000000000000000'))), - '0x1a100684fd68185060405f3f160e4bb6e034194336b547bdae323f888d5332' - ) - - // Non-empty address - assert.equal( - bytesToHex(getStem(verkle, Address.fromString('0x71562b71999873DB5b286dF957af199Ec94617f7'))), - '0x1540dfad7755b40be0768c6aa0a5096fbf0215e0e8cf354dd928a178346466' - ) - }) - - it('verifyProof(): should verify verkle proofs', () => { - // Src: Kaustinen6 testnet, block 71 state root (parent of block 72) - const prestateRoot = hexToBytes( - '0x64e1a647f42e5c2e3c434531ccf529e1b3e93363a40db9fc8eec81f492123510' - ) - const executionWitness = verkleBlockJSON.executionWitness as VerkleExecutionWitness - assert.isTrue(verifyProof(verkle, prestateRoot, executionWitness)) - }) - - it('verifyProof(): should return false for invalid verkle proofs', () => { - // Random preStateRoot - const prestateRoot = randomBytes(32) - const executionWitness = verkleBlockJSON.executionWitness as VerkleExecutionWitness - // Modify the proof to make it invalid - assert.isFalse(verifyProof(verkle, prestateRoot, executionWitness)) - }) -}) diff --git a/packages/verkle/test/internalNode.spec.ts b/packages/verkle/test/internalNode.spec.ts index 826006aa95..6aeb88169b 100644 --- a/packages/verkle/test/internalNode.spec.ts +++ b/packages/verkle/test/internalNode.spec.ts @@ -1,50 +1,61 @@ -import { equalsBytes, randomBytes } from '@ethereumjs/util' -import { assert, describe, it } from 'vitest' +import { type VerkleCrypto, equalsBytes, randomBytes } from '@ethereumjs/util' +import { loadVerkleCrypto } from 'verkle-cryptography-wasm' +import { assert, beforeAll, describe, it } from 'vitest' -import { NODE_WIDTH, VerkleNodeType } from '../src/node/index.js' +import { NODE_WIDTH, VerkleNodeType, decodeNode } from '../src/node/index.js' import { InternalNode } from '../src/node/internalNode.js' -import { POINT_IDENTITY } from '../src/util/crypto.js' describe('verkle node - internal', () => { + let verkleCrypto: VerkleCrypto + beforeAll(async () => { + verkleCrypto = await loadVerkleCrypto() + }) it('constructor should create an internal node', async () => { const commitment = randomBytes(32) - const depth = 2 - const node = new InternalNode({ commitment, depth }) + const node = new InternalNode({ commitment, verkleCrypto }) assert.equal(node.type, VerkleNodeType.Internal, 'type should be set') - assert.ok( - equalsBytes(node.commitment as unknown as Uint8Array, commitment), - 'commitment should be set' - ) - assert.equal(node.depth, depth, 'depth should be set') + assert.ok(equalsBytes(node.commitment, commitment), 'commitment should be set') // Children nodes should all default to null. assert.equal(node.children.length, NODE_WIDTH, 'number of children should equal verkle width') assert.ok( - node.children.every((child) => child === null), + node.children.every((child) => equalsBytes(child.commitment, verkleCrypto.zeroCommitment)), 'every children should be null' ) }) it('create method should create an internal node', async () => { - const depth = 3 - const node = InternalNode.create(depth) + const node = InternalNode.create(verkleCrypto) assert.equal(node.type, VerkleNodeType.Internal, 'type should be set') - assert.ok( - equalsBytes( - node.commitment as unknown as Uint8Array, - POINT_IDENTITY as unknown as Uint8Array - ), + assert.deepEqual( + node.commitment, + verkleCrypto.zeroCommitment, 'commitment should be set to point identity' ) - assert.equal(node.depth, depth, 'depth should be set') // Children nodes should all default to null. assert.equal(node.children.length, NODE_WIDTH, 'number of children should equal verkle width') assert.ok( - node.children.every((child) => child === null), + node.children.every((child) => equalsBytes(child.commitment, verkleCrypto.zeroCommitment)), 'every children should be null' ) }) + it('should serialize and deserialize a node', async () => { + const child = { + commitment: randomBytes(64), + path: randomBytes(10), + } + const children = new Array(256).fill({ commitment: new Uint8Array(64), path: new Uint8Array() }) + children[0] = child + const node = new InternalNode({ + children, + verkleCrypto, + commitment: verkleCrypto.zeroCommitment, + }) + const serialized = node.serialize() + const decoded = decodeNode(serialized, verkleCrypto) + assert.deepEqual((decoded as InternalNode).children[0].commitment, child.commitment) + }) }) diff --git a/packages/verkle/test/keys.spec.ts b/packages/verkle/test/keys.spec.ts deleted file mode 100644 index 0f41b1996e..0000000000 --- a/packages/verkle/test/keys.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { concatBytes, hexToBytes, intToBytes } from '@ethereumjs/util' -import { assert, describe, it } from 'vitest' - -import { LeafType, getKey } from '../src/index.js' - -describe('should generate valid tree keys', () => { - it('should generate valid keys for each LeafType', () => { - const stem = hexToBytes('0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d') - for (const leaf of [ - LeafType.Version, - LeafType.Balance, - LeafType.Nonce, - LeafType.CodeHash, - LeafType.CodeSize, - ]) { - const key = getKey(stem, leaf) - assert.equal(key.length, 32) - assert.deepEqual(key, concatBytes(stem, intToBytes(leaf))) - } - }) -}) diff --git a/packages/verkle/test/leafNode.spec.ts b/packages/verkle/test/leafNode.spec.ts index b3fdafa452..6e59f2537e 100644 --- a/packages/verkle/test/leafNode.spec.ts +++ b/packages/verkle/test/leafNode.spec.ts @@ -1,26 +1,28 @@ -import { equalsBytes, randomBytes } from '@ethereumjs/util' -import { assert, describe, it } from 'vitest' +import { type VerkleCrypto, equalsBytes, randomBytes } from '@ethereumjs/util' +import { loadVerkleCrypto } from 'verkle-cryptography-wasm' +import { assert, beforeAll, describe, it } from 'vitest' import { VerkleNodeType } from '../src/node/index.js' import { LeafNode } from '../src/node/leafNode.js' -import type { Point } from '../src/types.js' - describe('verkle node - leaf', () => { + let verkleCrypto = undefined as never as VerkleCrypto + beforeAll(async () => { + verkleCrypto = await loadVerkleCrypto() + }) it('constructor should create an leaf node', async () => { - const commitment = randomBytes(32) - const c1 = randomBytes(32) - const c2 = randomBytes(32) + const commitment = randomBytes(64) + const c1 = randomBytes(64) + const c2 = randomBytes(64) const stem = randomBytes(32) - const values = [randomBytes(32), randomBytes(32)] - const depth = 2 + const values = new Array(256).fill(randomBytes(32)) const node = new LeafNode({ - c1: c1 as unknown as Point, - c2: c2 as unknown as Point, + c1, + c2, commitment, - depth, stem, values, + verkleCrypto, }) assert.equal(node.type, VerkleNodeType.Leaf, 'type should be set') @@ -35,8 +37,25 @@ describe('verkle node - leaf', () => { values.every((value, index) => equalsBytes(value, node.values[index])), 'values should be set' ) - assert.equal(node.depth, depth, 'depth should be set') }) - it.todo('create method should create an leaf node') + it('create method should create an leaf node', async () => { + const key = randomBytes(32) + const value = randomBytes(32) + const values = new Array(256).fill(new Uint8Array(32)) + values[2] = value + const stem = key.slice(0, 31) + const node = await LeafNode.create(stem, values, verkleCrypto) + assert.ok(node instanceof LeafNode) + }) + + it('should update a commitment when setting a value', async () => { + const key = randomBytes(32) + const stem = key.slice(0, 31) + const values = new Array(256).fill(new Uint8Array(32)) + const node = await LeafNode.create(stem, values, verkleCrypto) + assert.deepEqual(node.c1, verkleCrypto.zeroCommitment) + node.setValue(0, randomBytes(32)) + assert.notDeepEqual(node.c1, verkleCrypto.zeroCommitment) + }) }) diff --git a/packages/verkle/test/util/bytes.spec.ts b/packages/verkle/test/util/bytes.spec.ts new file mode 100644 index 0000000000..850a2bdb14 --- /dev/null +++ b/packages/verkle/test/util/bytes.spec.ts @@ -0,0 +1,47 @@ +import { assert, describe, it } from 'vitest' + +import { matchingBytesLength } from '../../src/util/bytes.js' + +describe('matchingBytesLength', () => { + it('should return 0 when both arrays are empty', () => { + const bytes1 = new Uint8Array([]) + const bytes2 = new Uint8Array([]) + assert.equal(matchingBytesLength(bytes1, bytes2), 0) + }) + + it('should return 0 when one of the arrays is empty', () => { + const bytes1 = new Uint8Array([1, 2, 3]) + const bytes2 = new Uint8Array([]) + assert.equal(matchingBytesLength(bytes1, bytes2), 0) + }) + + it('should return 0 when arrays have no matching elements', () => { + const bytes1 = new Uint8Array([1, 2, 3]) + const bytes2 = new Uint8Array([4, 5, 6]) + assert.equal(matchingBytesLength(bytes1, bytes2), 0) + }) + + it('should handle arrays with same elements but different lengths', () => { + const bytes1 = new Uint8Array([1, 2, 3]) + const bytes2 = new Uint8Array([1, 2, 3, 4]) + assert.equal(matchingBytesLength(bytes1, bytes2), 3) + }) + + it('should handle arrays with matching elements at end', () => { + const bytes1 = new Uint8Array([1, 2, 3]) + const bytes2 = new Uint8Array([0, 1, 2, 3]) + assert.equal(matchingBytesLength(bytes1, bytes2), 0) + }) + + it('should handle arrays with matching elements at start', () => { + const bytes1 = new Uint8Array([1, 2, 3]) + const bytes2 = new Uint8Array([1, 2, 3, 4, 5]) + assert.equal(matchingBytesLength(bytes1, bytes2), 3) + }) + + it('should handle arrays with large number of elements', () => { + const bytes1 = new Uint8Array(Array.from({ length: 1000000 }, (_, i) => i)) + const bytes2 = new Uint8Array(Array.from({ length: 1000000 }, (_, i) => i)) + assert.equal(matchingBytesLength(bytes1, bytes2), 1000000) + }) +}) diff --git a/packages/verkle/test/verkle.spec.ts b/packages/verkle/test/verkle.spec.ts index 9ce064e07b..df6a78a14e 100644 --- a/packages/verkle/test/verkle.spec.ts +++ b/packages/verkle/test/verkle.spec.ts @@ -1,14 +1,30 @@ -import { equalsBytes, hexToBytes } from '@ethereumjs/util' -import { assert, describe, it } from 'vitest' +import { MapDB, equalsBytes, hexToBytes } from '@ethereumjs/util' +import { loadVerkleCrypto } from 'verkle-cryptography-wasm' +import { assert, beforeAll, describe, it } from 'vitest' +import { + InternalNode, + LeafNode, + VerkleNodeType, + decodeNode, + matchingBytesLength, +} from '../src/index.js' import { VerkleTree } from '../src/verkleTree.js' -import type { PrefixedHexString } from '@ethereumjs/util' +import type { VerkleNode } from '../src/index.js' +import type { PrefixedHexString, VerkleCrypto } from '@ethereumjs/util' // Testdata from https://github.com/gballet/go-ethereum/blob/kaustinen-with-shapella/trie/verkle_test.go const presentKeys = [ + // Two keys with the same stem but different suffixes '0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d01', + '0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', + // A key with a partially matching stem 0x318d to above 2 keys + '0x318dfa512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', + // A key with a partially matching stem 0x318dfa51 to above key + '0x318dfa513b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', '0xe6ed6c222e3985050b4fc574b136b0a42c63538e9ab970995cd418ba8e526400', + '0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', '0x18fb432d3b859ec3a1803854e8cceea75d092e52d0d4a4398d13022496745a02', '0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', '0x18fb432d3b859ec3a1803854e8cceea75d092e52d0d4a4398d13022496745a04', @@ -46,17 +62,189 @@ const absentKeys = [ ].map((key) => hexToBytes(key as PrefixedHexString)) describe('Verkle tree', () => { - it.todo('should insert and retrieve values', async () => { - const tree = new VerkleTree() - for (let i = 0; i < presentKeys.length; i++) { + it.skip('should insert and retrieve values', async () => { + const verkleCrypto = await loadVerkleCrypto() + const tree = await VerkleTree.create({ + verkleCrypto, + db: new MapDB(), + }) + + const res = await tree.findPath(presentKeys[0]) + + assert.ok(res.node === null, 'should not find a node when the key is not present') + assert.deepEqual(res.remaining, presentKeys[0]) + + // Test that two keys with the same stem store values in the same leaf node + for (let i = 0; i < 2; i++) { + await tree.put(presentKeys[i], values[i]) + } + for (let i = 0; i < 2; i++) { + const retrievedValue = await tree.get(presentKeys[i]) + if (retrievedValue === undefined) { + assert.fail('Value not found') + } + assert.ok(equalsBytes(retrievedValue, values[i])) + } + + // Test that one key that partially matches the stems of the two existing keys is inserted (with appropriate internal nodes inserted as well) + for (let i = 2; i < 3; i++) { await tree.put(presentKeys[i], values[i]) } - for (let i = 0; i < presentKeys.length; i++) { + for (let i = 2; i < 3; i++) { const retrievedValue = await tree.get(presentKeys[i]) - if (retrievedValue === null) { + if (retrievedValue === undefined) { assert.fail('Value not found') } assert.ok(equalsBytes(retrievedValue, values[i])) } + + // Test that one key that with further partial match to the previous key is inserted (with appropriate internal nodes inserted as well) + for (let i = 3; i < 4; i++) { + await tree.put(presentKeys[i], values[i]) + } + for (let i = 3; i < 4; i++) { + const retrievedValue = await tree.get(presentKeys[i]) + if (retrievedValue === undefined) { + assert.fail('Value not found') + } + assert.ok(equalsBytes(retrievedValue, values[i])) + } + + // Get path to node at depth 2 and verify that the whole key is used + const pathToDeepNode = await tree.findPath(presentKeys[2].slice(0, 31)) + assert.ok(pathToDeepNode.node !== null) + assert.equal(pathToDeepNode.remaining.length, 0) + // Verify that findPath returns a path that demonstrates the nonexistence of a key + // by returning only the root node (in this instance where the trie has only a root internal node and 1 leaf node) + // with a different stem than the one passed to `findPath` + const pathToNonExistentNode = await tree.findPath(absentKeys[0]) + assert.equal(pathToNonExistentNode.node, null) + assert.equal(pathToNonExistentNode.stack.length, 2, 'contains the root node in the stack') + }) +}) + +describe('findPath validation', () => { + let verkleCrypto: VerkleCrypto + beforeAll(async () => { + verkleCrypto = await loadVerkleCrypto() + }) + it('should find the path to various leaf nodes', async () => { + const keys = [ + // Two keys with the same stem but different suffixes + '0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d01', + '0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', + // A key with a partially matching stem 0x318d to above 2 keys + '0x318dfa512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', + // A key with a partially matching stem 0x318dfa51 to above key + '0x318dfa513b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d02', + ] + const values = [ + '0x320122e8584be00d000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0300000000000000000000000000000000000000000000000000000000000000', + ] + const trie = await VerkleTree.create({ + verkleCrypto, + db: new MapDB(), + }) + + await trie['_createRootNode']() + + let putStack: [Uint8Array, VerkleNode][] = [] + const stem1 = hexToBytes(keys[0]).slice(0, 31) + // Create first leaf node + const leafNode1 = await LeafNode.create( + stem1, + new Array(256).fill(new Uint8Array(32)), + verkleCrypto + ) + + leafNode1.setValue(hexToBytes(keys[0])[31], hexToBytes(values[0])) + leafNode1.setValue(hexToBytes(keys[1])[31], hexToBytes(values[1])) + + putStack.push([leafNode1.hash(), leafNode1]) + + // Pull root node from DB + const rawNode = await trie['_db'].get(trie.root()) + const rootNode = decodeNode(rawNode!, verkleCrypto) as InternalNode + // Update root node with commitment from leaf node + rootNode.setChild(stem1[0], { commitment: leafNode1.commitment, path: stem1 }) + trie.root(verkleCrypto.serializeCommitment(rootNode.commitment)) + putStack.push([trie.root(), rootNode]) + await trie.saveStack(putStack) + + // Verify that path to leaf node can be found from stem + const res = await trie.findPath(stem1) + assert.deepEqual(res.node?.commitment, leafNode1.commitment) + + // Retrieve a value from the leaf node + const val1 = await trie.get(hexToBytes(keys[1])) + assert.deepEqual(val1, hexToBytes(values[1])) + + // Put a second leaf node in the tree with a partially matching stem + putStack = [] + const stem2 = hexToBytes(keys[2]).slice(0, 31) + + // Find path to closest node in tree + const foundPath = await trie.findPath(stem2) + + // Confirm node with stem2 doesn't exist in trie + assert.equal(foundPath.node, null) + + // Create new leaf node + const leafNode2 = await LeafNode.create( + stem2, + new Array(256).fill(new Uint8Array(32)), + verkleCrypto + ) + leafNode2.setValue(hexToBytes(keys[2])[31], hexToBytes(values[2])) + putStack.push([leafNode2.hash(), leafNode2]) + + const nearestNode = foundPath.stack.pop()![0] + // Verify that another leaf node is "nearest" node + assert.equal(nearestNode.type, VerkleNodeType.Leaf) + assert.deepEqual(nearestNode, leafNode1) + + // Compute the portion of stem1 and stem2 that match (i.e. the partial path closest to stem2) + // Note: We subtract 1 since we are using 0-indexed arrays + const partialMatchingStemIndex = matchingBytesLength(stem1, stem2) - 1 + // Find the path to the new internal node (the matching portion of stem1 and stem2) + const internalNode1Path = stem1.slice(0, partialMatchingStemIndex) + // Create new internal node + const internalNode1 = InternalNode.create(verkleCrypto) + + // Update the child references for leafNode1 and leafNode 2 + internalNode1.setChild(stem1[partialMatchingStemIndex], { + commitment: nearestNode.commitment, + path: (nearestNode as LeafNode).stem, + }) + internalNode1.setChild(stem2[partialMatchingStemIndex], { + commitment: leafNode2.commitment, + path: stem2, + }) + + putStack.push([internalNode1.hash(), internalNode1]) + // Update rootNode child reference for internal node 1 + + const rootNodeFromPath = foundPath.stack.pop()![0] as InternalNode + // Confirm node from findPath matches root + assert.deepEqual(rootNodeFromPath, rootNode) + rootNodeFromPath.setChild(internalNode1Path[0], { + commitment: internalNode1.commitment, + path: internalNode1Path, + }) + trie.root(verkleCrypto.serializeCommitment(rootNodeFromPath.commitment)) + putStack.push([trie.root(), rootNodeFromPath]) + await trie.saveStack(putStack) + let res2 = await trie.findPath(stem1) + + assert.equal(res2.remaining.length, 0, 'confirm full path was found') + assert.equal(res2.stack.length, 2, 'confirm node is at depth 2') + res2 = await trie.findPath(stem2) + assert.equal(res2.remaining.length, 0, 'confirm full path was found') + assert.equal(res2.stack.length, 2, 'confirm node is at depth 2') + const val2 = await trie.get(hexToBytes(keys[2])) + assert.deepEqual(val2, hexToBytes(values[2]), 'confirm values[2] can be retrieved from trie') }) }) diff --git a/packages/vm/package.json b/packages/vm/package.json index b318acd98f..0caeb614af 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -72,10 +72,9 @@ "@ethereumjs/trie": "^6.2.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", - "@ethereumjs/verkle": "^0.0.2", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.3", - "mcl-wasm": "^1.4.0" + "mcl-wasm": "^1.5.0" }, "devDependencies": { "@ethersproject/abi": "^5.0.12", diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index 3e727cdbfd..6d8d2f4f48 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -89,11 +89,9 @@ const accumulateEIP7002Requests = async ( for (let startByte = 0; startByte < resultsBytes.length; startByte += 76) { const slicedBytes = resultsBytes.slice(startByte, startByte + 76) const sourceAddress = slicedBytes.slice(0, 20) // 20 Bytes - const validatorPublicKey = slicedBytes.slice(20, 68) // 48 Bytes + const validatorPubkey = slicedBytes.slice(20, 68) // 48 Bytes const amount = bytesToBigInt(unpadBytes(slicedBytes.slice(68, 76))) // 8 Bytes / Uint64 - requests.push( - WithdrawalRequest.fromRequestData({ sourceAddress, validatorPublicKey, amount }) - ) + requests.push(WithdrawalRequest.fromRequestData({ sourceAddress, validatorPubkey, amount })) } } diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index e38f6035f7..5c5a15343b 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -12,18 +12,18 @@ import { BIGINT_8, GWEI_TO_WEI, KECCAK256_RLP, + bigIntToAddressBytes, bigIntToBytes, - bigIntToHex, bytesToHex, concatBytes, equalsBytes, + getVerkleTreeIndexesForStorageSlot, hexToBytes, intToBytes, setLengthLeft, short, unprefixedHexToBytes, } from '@ethereumjs/util' -import { getTreeIndexesForStorageSlot } from '@ethereumjs/verkle' import debugDefault from 'debug' import { Bloom } from './bloom/index.js' @@ -488,8 +488,8 @@ export async function accumulateParentBlockHash( if (!this.common.isActivatedEIP(2935)) { throw new Error('Cannot call `accumulateParentBlockHash`: EIP 2935 is not active') } - const historyAddress = Address.fromString( - bigIntToHex(this.common.param('vm', 'historyStorageAddress')) + const historyAddress = new Address( + bigIntToAddressBytes(this.common.param('vm', 'historyStorageAddress')) ) const historyServeWindow = this.common.param('vm', 'historyServeWindow') @@ -513,7 +513,7 @@ export async function accumulateParentBlockHash( // generate access witness if (vm.common.isActivatedEIP(6800)) { - const { treeIndex, subIndex } = getTreeIndexesForStorageSlot(ringKey) + const { treeIndex, subIndex } = getVerkleTreeIndexesForStorageSlot(ringKey) // just create access witnesses without charging for the gas ;( vm.stateManager as StatelessVerkleStateManager diff --git a/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts b/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts index a29ae9c4f6..ecec388476 100644 --- a/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts +++ b/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts @@ -101,16 +101,16 @@ const deploymentConfigs = [ // may 25 configuration with set on the lines of 4788 [ // contract code - '0x3373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35600143035500', + '0x3373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500', // deployment tx input - '0x60608060095f395ff33373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35600143035500', + '0x60648060095f395ff33373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500', // v r s ['0x1b', '0x539', '0x1b9b6eb1f0'], // sender, hash, deployed address [ - '0x72eed28860ac985f1ec32306564b5926ea7c0b70', - '0xe43ec833884324f31c2e8314534d5b15233d84f32f05a05ea2a45649b587a9df', - '0xcc766763fcc59487cdab9f21487174417b1fa282', + '0xe473f7e92ba2490e9fcbbe8bb9c3be3adbb74efc', + '0x3c769a03d6e2212f1d26ab59ba797dce0900df29ffd23c1dd391fd6b217973ad', + '0x0aae40965e6800cd9b1f4b05ff21581047e3f91e', ], ], ] diff --git a/packages/vm/test/api/EIPs/eip-6800-verkle.spec.ts b/packages/vm/test/api/EIPs/eip-6800-verkle.spec.ts index d416392d3d..1f7d5657cc 100644 --- a/packages/vm/test/api/EIPs/eip-6800-verkle.spec.ts +++ b/packages/vm/test/api/EIPs/eip-6800-verkle.spec.ts @@ -4,6 +4,7 @@ import { EVM } from '@ethereumjs/evm' import { StatelessVerkleStateManager } from '@ethereumjs/statemanager' import { TransactionFactory } from '@ethereumjs/tx' import { hexToBytes } from '@ethereumjs/util' +import { loadVerkleCrypto } from 'verkle-cryptography-wasm' import { describe, it } from 'vitest' import * as verkleBlockJSON from '../../../../statemanager/test/testdata/verkleKaustinen6Block72.json' @@ -31,7 +32,8 @@ const block = Block.fromBlockData({ ...verkleBlockJSON, transactions: decodedTxs describe('EIP 6800 tests', () => { it('successfully run transactions statelessly using the block witness', async () => { - const verkleStateManager = await StatelessVerkleStateManager.create({ common }) + const verkleCrypto = await loadVerkleCrypto() + const verkleStateManager = new StatelessVerkleStateManager({ common, verkleCrypto }) const evm = await EVM.create({ common, stateManager: verkleStateManager }) const vm = await VM.create({ common, diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 8805e0d390..950fe6e865 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@ethereumjs/util": "^9.0.3", - "@scure/base": "^1.1.5", + "@scure/base": "^1.1.7", "ethereum-cryptography": "^2.1.3", "js-md5": "^0.8.3", "uuid": "^9.0.1"