diff --git a/package-lock.json b/package-lock.json index 541df8edad..4d8b8c129a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1694,6 +1694,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@chainsafe/as-sha256": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", + "integrity": "sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==" + }, "node_modules/@chainsafe/libp2p-noise": { "version": "4.1.2", "license": "MIT", @@ -1756,6 +1761,23 @@ "node": ">=12.0.0" } }, + "node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz", + "integrity": "sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==", + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "node_modules/@chainsafe/ssz": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.10.0.tgz", + "integrity": "sha512-d03CXdXdRNwSEgxYRJdh+9yejaeQp8vwUhlSevEt7OEoMncyDl4toHZqDRfXXp5Deqq7IxQ+f5ZaEtZvD7h6LA==", + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.4.2" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "license": "MIT", @@ -18205,6 +18227,7 @@ "version": "8.0.3", "license": "MPL-2.0", "dependencies": { + "@chainsafe/ssz": "^0.10.0", "@ethereumjs/rlp": "^4.0.0-beta.2", "async": "^3.2.4", "ethereum-cryptography": "^1.1.2" @@ -19338,6 +19361,11 @@ "version": "0.2.3", "dev": true }, + "@chainsafe/as-sha256": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", + "integrity": "sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==" + }, "@chainsafe/libp2p-noise": { "version": "4.1.2", "requires": { @@ -19391,6 +19419,23 @@ } } }, + "@chainsafe/persistent-merkle-tree": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz", + "integrity": "sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==", + "requires": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "@chainsafe/ssz": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.10.0.tgz", + "integrity": "sha512-d03CXdXdRNwSEgxYRJdh+9yejaeQp8vwUhlSevEt7OEoMncyDl4toHZqDRfXXp5Deqq7IxQ+f5ZaEtZvD7h6LA==", + "requires": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.4.2" + } + }, "@colors/colors": { "version": "1.5.0" }, @@ -19767,6 +19812,7 @@ "@ethereumjs/util": { "version": "file:packages/util", "requires": { + "@chainsafe/ssz": "*", "@ethereumjs/rlp": "^4.0.0-beta.2", "@types/bn.js": "^5.1.0", "@types/secp256k1": "^4.0.1", diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index f603e44881..d4702d66ee 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -11,6 +11,7 @@ import { bufferToHex, intToHex, isHexPrefixed, + ssz, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak' import { ethers } from 'ethers' @@ -52,6 +53,14 @@ export class Block { return trie.root() } + /** + * Returns the ssz root for array of withdrawal transactions. + * @param wts array of Withdrawal to compute the root of + */ + public static async generateWithdrawalsSSZRoot(withdrawals: Withdrawal[]) { + ssz.Withdrawals.hashTreeRoot(withdrawals.map((wt) => wt.toValue())) + } + /** * Returns the txs trie root for array of TypedTransaction * @param txs array of TypedTransaction to compute the root of diff --git a/packages/util/karma.conf.js b/packages/util/karma.conf.js index ae20d1e865..3703048371 100644 --- a/packages/util/karma.conf.js +++ b/packages/util/karma.conf.js @@ -8,6 +8,9 @@ module.exports = function (config) { karmaTypescriptConfig: { bundlerOptions: { entrypoints: /\.spec\.ts$/, + acornOptions: { + ecmaVersion: 11, + }, }, tsconfig: './tsconfig.json', }, diff --git a/packages/util/package.json b/packages/util/package.json index 73d7e21779..8bae0f9b18 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -83,7 +83,8 @@ "tsc": "../../config/cli/ts-compile.sh" }, "dependencies": { - "@ethereumjs/rlp": "^4.0.0-beta.2", + "@chainsafe/ssz": "^0.10.0", + "@ethereumjs/rlp": "^4.0.0", "async": "^3.2.4", "ethereum-cryptography": "^1.1.2" }, diff --git a/packages/util/src/constants.ts b/packages/util/src/constants.ts index e066082716..0b25b0fe79 100644 --- a/packages/util/src/constants.ts +++ b/packages/util/src/constants.ts @@ -63,3 +63,5 @@ export const KECCAK256_RLP = Buffer.from(KECCAK256_RLP_S, 'hex') * RLP encoded empty string */ export const RLP_EMPTY_STRING = Buffer.from([0x80]) + +export const MAX_WITHDRAWALS_PER_PAYLOAD = 16 diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index d2737fe8d0..51bcfe86ed 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -33,6 +33,11 @@ export * from './signature' */ export * from './bytes' +/** + * SSZ containers + */ +export * as ssz from './ssz' + /** * Helpful TypeScript types */ diff --git a/packages/util/src/ssz.ts b/packages/util/src/ssz.ts new file mode 100644 index 0000000000..1b8462148f --- /dev/null +++ b/packages/util/src/ssz.ts @@ -0,0 +1,24 @@ +import { + ByteVectorType, + ContainerType, + ListCompositeType, + UintBigintType, + UintNumberType, +} from '@chainsafe/ssz' + +import { MAX_WITHDRAWALS_PER_PAYLOAD } from './constants' + +export const UintNum64 = new UintNumberType(8) +export const UintBigInt64 = new UintBigintType(8) +export const Bytes20 = new ByteVectorType(20) + +export const Withdrawal = new ContainerType( + { + index: UintBigInt64, + validatorIndex: UintBigInt64, + address: Bytes20, + amount: UintBigInt64, + }, + { typeName: 'Withdrawal', jsonCase: 'eth2' } +) +export const Withdrawals = new ListCompositeType(Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD) diff --git a/packages/util/src/withdrawal.ts b/packages/util/src/withdrawal.ts index 8e9d8d51e6..88a9ef5918 100644 --- a/packages/util/src/withdrawal.ts +++ b/packages/util/src/withdrawal.ts @@ -103,6 +103,15 @@ export class Withdrawal { return Withdrawal.toBufferArray(this) } + toValue() { + return { + index: this.index, + validatorIndex: this.validatorIndex, + address: this.address.buf, + amount: this.amount, + } + } + toJSON() { return { index: bigIntToHex(this.index), diff --git a/packages/util/test/ssz.spec.ts b/packages/util/test/ssz.spec.ts new file mode 100644 index 0000000000..695e800002 --- /dev/null +++ b/packages/util/test/ssz.spec.ts @@ -0,0 +1,94 @@ +import * as tape from 'tape' + +import { Withdrawal, ssz } from '../src' +const withdrawalsData = [ + { + index: BigInt(0), + validatorIndex: BigInt(65535), + address: Buffer.from('0000000000000000000000000000000000000000', 'hex'), + amount: BigInt('0'), + }, + { + index: BigInt(1), + validatorIndex: BigInt(65536), + address: Buffer.from('0100000000000000000000000000000000000000', 'hex'), + amount: BigInt('04523128485832663883'), + }, + { + index: BigInt(2), + validatorIndex: BigInt(65537), + address: Buffer.from('0200000000000000000000000000000000000000', 'hex'), + amount: BigInt('09046256971665327767'), + }, + { + index: BigInt(4), + validatorIndex: BigInt(65538), + address: Buffer.from('0300000000000000000000000000000000000000', 'hex'), + amount: BigInt('13569385457497991651'), + }, + { + index: BigInt(4), + validatorIndex: BigInt(65539), + address: Buffer.from('0400000000000000000000000000000000000000', 'hex'), + amount: BigInt('18446744073709551615'), + }, + { + index: BigInt(5), + validatorIndex: BigInt(65540), + address: Buffer.from('0500000000000000000000000000000000000000', 'hex'), + amount: BigInt('02261564242916331941'), + }, + { + index: BigInt(6), + validatorIndex: BigInt(65541), + address: Buffer.from('0600000000000000000000000000000000000000', 'hex'), + amount: BigInt('02713877091499598330'), + }, + { + index: BigInt(7), + validatorIndex: BigInt(65542), + address: Buffer.from('0700000000000000000000000000000000000000', 'hex'), + amount: BigInt('03166189940082864718'), + }, +] + +tape('ssz', (t) => { + t.test('withdrawals', (st) => { + const withdrawals = withdrawalsData.map((wt) => Withdrawal.fromWithdrawalData(wt)) + const withdrawalsValue = withdrawals.map((wt) => wt.toValue()) + const sszValues = ssz.Withdrawals.toViewDU(withdrawalsData) + .toValue() + .map((wt) => { + wt.address = Buffer.from(wt.address) + return wt + }) + st.deepEqual(sszValues, withdrawalsValue, 'sszValues should be same as withdrawalsValue') + const withdrawalsRoot = ssz.Withdrawals.hashTreeRoot(withdrawalsValue) + st.equal( + Buffer.from(withdrawalsRoot).toString('hex'), + 'bd97f65e513f870484e85927510acb291fcfb3e593c05ab7f21f206921264946', + 'ssz root should match' + ) + st.end() + }) + + const specWithdrawals = [ + // https://github.com/ethereum/consensus-spec-tests/tree/v1.3.0-rc.1/tests/mainnet/capella/ssz_static/Withdrawal/ssz_random/case_0 + { + index: BigInt('17107150653359250726'), + validatorIndex: BigInt('1906681273455760070'), + address: Buffer.from('02ab1379b6334b58df82c85d50ff1214663cba20', 'hex'), + amount: BigInt('5055030296454530815'), + }, + ] + + t.test('match spec v1.3.0-rc.1', (st) => { + const withdrawalsRoot = ssz.Withdrawal.hashTreeRoot(specWithdrawals[0]) + st.equal( + Buffer.from(withdrawalsRoot).toString('hex'), + 'ed9cec6fb8ee22b146059d02c38940cca1dd22a00d0132b000999b983fceff95', + 'ssz root should match' + ) + st.end() + }) +}) diff --git a/packages/util/test/withdrawal.spec.ts b/packages/util/test/withdrawal.spec.ts index 9396ba4e83..a521255b87 100644 --- a/packages/util/test/withdrawal.spec.ts +++ b/packages/util/test/withdrawal.spec.ts @@ -83,12 +83,18 @@ tape('Withdrawal', (t) => { st.end() }) - t.test('fromValuesArray and toJSON', (st) => { + t.test('fromValuesArray, toJSON and toValue', (st) => { const withdrawals = (gethWithdrawalsBuffer as WithdrawalBuffer[]).map( Withdrawal.fromValuesArray ) const withdrawalsJson = withdrawals.map((wt) => wt.toJSON()) st.deepEqual(withdrawalsGethVector, withdrawalsJson, 'Withdrawals json should match') + + const withdrawalsValue = withdrawals.map((wt) => wt.toValue()) + st.deepEqual( + withdrawalsValue.map((wt) => `0x${wt.address.toString('hex')}`), + withdrawalsJson.map((wt) => wt.address) + ) st.end() }) })