From 3dfa90390a31bc33b8acdd46e49f1123373183f6 Mon Sep 17 00:00:00 2001 From: Ardis Lu Date: Mon, 16 Sep 2024 23:35:29 -0700 Subject: [PATCH] Add initial support for zkSync Era --- calculate.js | 75 ++++++++++++++++++- calculate.test.js | 182 +++++++++++++++++++++++++++++++++++++++++++++- index.html | 24 ++++-- 3 files changed, 270 insertions(+), 11 deletions(-) diff --git a/calculate.js b/calculate.js index ae0ecbc..df20fd7 100644 --- a/calculate.js +++ b/calculate.js @@ -5,6 +5,11 @@ function hex(array) { return `0x${[...array].map(v => v.toString(16).padStart(2, '0')).join('')}`; } +// Hex string to Uint8Array +function arr(hex) { + return Uint8Array.from(hex.replace('0x', '').match(/.{2}/g), v => parseInt(v, 16)); +} + // Calculate the header, RLP-encoded header, and Keccak-256 hash of RLP function calculate(block) { // Header fields available at genesis @@ -76,9 +81,75 @@ function calculate(block) { return { header, - rlp: hex(rlp), + encoded: hex(rlp), hash: hex(hash) }; } -export { calculate }; \ No newline at end of file +// zkSync Era ABI-encodes its block header parameters instead of RLP-encoding +// https://docs.zksync.io/zk-stack/concepts/blocks#l2-blockhash-calculation-and-storage +function calculateZkSync(block) { + // Block number 13234791 was not documented in the upgrade spec but retrieved from manual testing + // https://github.com/zkSync-Community-Hub/zksync-developers/discussions/87 + if (Number(block.number) < 13234791) { // "Legacy blockhash" is keccak256(abi.encodePacked(uint32(_blockNumber))) + const header = [Number(block.number)]; + + // abi.encodePacked(uint32(_blockNumber)) is the block number literal as a big-endian uint32 + const n = new DataView(new ArrayBuffer(4)); + n.setUint32(0, Number(block.number), false); // Forces big-endian regardless of platform + const data = new Uint8Array(n.buffer); + + const hash = keccak256(data); + + return { + header, + encoded: hex(data), + hash: hex(hash) + }; + } + + // Non-legacy blockhash is keccak256(abi.encode(_blockNumber, _blockTimestamp, _prevL2BlockHash, _blockTxsRollingHash)) + // where "_blockTxsRollingHash" is defined as: + // _blockTxsRollingHash = 0 for an empty block. + // _blockTxsRollingHash = keccak(0, tx1_hash) for a block with one tx. + // _blockTxsRollingHash = keccak(keccak(0, tx1_hash), tx2_hash) for a block with two txs, etc. + // Note that abi.encode() is used to concat bytes, i.e. "0" is uint32(0). + function blockTxsRollingHash(transactions) { + if (transactions.length === 0) { + return new Uint8Array(32); + } + let hash = keccak256(Uint8Array.from([...new Uint8Array(32), ...arr(transactions[0])])); + for (let i = 1; i < transactions.length; i++) { + hash = keccak256(Uint8Array.from([...hash, ...arr(transactions[i])])); + } + return hash; + } + + const header = [ + Number(block.number), + Number(block.timestamp), + block.parentHash, + block.transactions + ]; + + // Since each value is equal to or less than 32 bytes, abi.encode just gives each value a 32-byte slot (big-endian) + const data = new Uint8Array(32 * 4); + const n = new DataView(new ArrayBuffer(4)); // block.number + n.setUint32(0, Number(block.number), false); + data.set(new Uint8Array(n.buffer), 32 - 4); + const t = new DataView(new ArrayBuffer(4)); // block.timestamp + t.setUint32(0, Number(block.timestamp), false); + data.set(new Uint8Array(t.buffer), 32 * 2 - 4); + data.set(arr(block.parentHash), 32 * 2); // block.parentHash + data.set(blockTxsRollingHash(block.transactions), 32 * 3); // blockTxsRollingHash + + const hash = keccak256(data); + + return { + header, + encoded: hex(data), + hash: hex(hash) + } +} + +export { calculate, calculateZkSync }; \ No newline at end of file diff --git a/calculate.test.js b/calculate.test.js index f1ca8a1..81ca57e 100644 --- a/calculate.test.js +++ b/calculate.test.js @@ -1,7 +1,7 @@ import { suite, test } from 'node:test'; import { deepStrictEqual } from 'node:assert/strict'; -import { calculate } from './calculate.js'; +import { calculate, calculateZkSync } from './calculate.js'; suite('Ethereum', { concurrency: true }, () => { test('Block number 0', { concurrency: true }, () => { @@ -744,4 +744,184 @@ suite('Polygon zkEVM', { concurrency: true }, () => { }); deepStrictEqual(hash, '0xdff4c21929f8134749c9f928db4fde6f3053cc1f315e23bb6921b654ad6ab43d'); }); +}); + +suite('zkSync Era', { concurrency: true }, () => { + test('Block number 0', { concurrency: true }, () => { + const { hash } = calculateZkSync({ + "baseFeePerGas": "0x0", + "difficulty": "0x0", + "extraData": "0x", + "gasLimit": "0xffffffff", + "gasUsed": "0x0", + "l1BatchNumber": "0x0", + "l1BatchTimestamp": "0x0", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner": "0x0000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sealFields": [], + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x0", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x0", + "totalDifficulty": "0x0", + "transactions": [], + "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncles": [] + }); + deepStrictEqual(hash, '0xe8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c'); + }); + + test('Block number 13234790 (legacy)', { concurrency: true }, () => { + const { hash } = calculateZkSync({ + "baseFeePerGas": "0xee6b280", + "difficulty": "0x0", + "extraData": "0x", + "gasLimit": "0xffffffff", + "gasUsed": "0x0", + "l1BatchNumber": "0x30aea", + "l1BatchTimestamp": "0x64f9a7c2", + "logsBloom": "0x00000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000048000000000000020000000000000000000000080000000000000000000000000000000000000000000000000000000010000000000000000000000000000004000000000000000000000000000000000000000000000000000000100000008000000000000000000000000000000000000000000000000002008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner": "0x0000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0xc9f266", + "parentHash": "0x301d8775e7ae378b1673f3b0fa665934a15f28c095b8f48c9e12ffd032f1a9b0", + "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sealFields": [], + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x0", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x64f9a7e0", + "totalDifficulty": "0x0", + "transactions": [], + "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncles": [] + }); + deepStrictEqual(hash, '0x21b1550b3ec306852d5e736df500339db011c6c11067a90a6ffc73cc957e2b37'); + }); + + test('Block number 13234791 (non-legacy)', { concurrency: true }, () => { + const { hash } = calculateZkSync({ + "baseFeePerGas": "0xee6b280", + "difficulty": "0x0", + "extraData": "0x", + "gasLimit": "0xffffffff", + "gasUsed": "0x32b4e64", + "l1BatchNumber": "0x30aeb", + "l1BatchTimestamp": "0x64f9a7e2", + "logsBloom": "0x5420040030a475de916122308471025005084011113750081801118180062112221ae00804b0478244330a3a460340a282610a4aa01858400312503926ac220824480100d0632445a679080948006d2b00400511884e0aa502134c0140060849638400067e1608800765054372032b8245a8892a092c4404a04004106d101a104100252c001080488864cc2d1050688c9210992a3b48823d14dc106461b06c838201c4ca4801124621000c81d42210005401613808d2c00a3aa405b104662000008091022c800a00a086081402130a00414700241d888430f52512d2000776aa72b0688ca00488000484b240195800889825145401271692b76322a6011ce16a", + "miner": "0x0000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0xc9f267", + "parentHash": "0x21b1550b3ec306852d5e736df500339db011c6c11067a90a6ffc73cc957e2b37", + "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sealFields": [], + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x0", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x64f9a7e2", + "totalDifficulty": "0x0", + "transactions": [ + "0xd04ca4c3c25af78fa82784dcb1b4c173003c7497c1ee2082ab142d4bc7b8332e", + "0x989a8446d14a5b94ad1f23c884e661531f1c36478d659bd98a4c567803510a6f", + "0x609648b8b452457884bb2e1722545629f72de9b3e2c4f0add45e0c72995c8c9c", + "0x16dc81dab9c12ddedd8db9545ea3e0911738d9a4b01eab3105ad8ff3effebeca", + "0xd9b562117e306ff6be5bc00bdf871083f53011cea07a1f38ed2b48141f85881c", + "0x04a5a37d67881b12d73cb27719b2606e156679e6ce69a0f6cd4b2f2a88d3475e", + "0xd5aed0368799d25e2ea2a5310d01fbfb2f20e94f8d1db63fc7fea9a76c3ce0a1", + "0x0682727377e5e0a114bdb8f13c7d00ac5c2f29315aece3dacd6bb4beb656c8ef", + "0xcb169deedd4799131591b62ab9597d1a9a48a8105be694c6c735a614c11c4632", + "0x8897b545ec8cb3a509186bc4015afddcbe639b277a098fa7d471081dfb73f78a", + "0xf02956fdb0f70018415f511793965d4807afe42c26b53f64d59feda196fb63d1", + "0xc6c97e5f7a71a63382d39fd2420f378948c69123b5d441ee2a84218a1c7f7f70", + "0xc7dbcd58c0f9dfa8e87d5612444b9da30d3cf6303f34febedc16d18f77c36eab", + "0xc438314ca431d24ec8045c0a92a2df0c6d16baf9112f4f0494d406f331042815", + "0x8ed7aae7a82899f39bdabf453e3079e88487656db12cf7dcdfd7192f601d30f1", + "0xb107ba4402795ec4e757667f1ea991f8c18df2ffed903104eb24d39300bde350", + "0x011642892351333d8e3eb8bd5c70de10b4f79cb97d66c0d47476ddc6e6ea44f1", + "0x0df69c4a45e8958afdbb089cce4816c3ee8eb3831c94f538487ddf56aa7407c9", + "0xdbdd608c293e1c32c05ecaa2f34b095fc7f2cf3b4912c831a27e126433b268c3", + "0xbb577fd9708c66404826d1ea76af553349caed6c5a834961e6d186b140b81bed", + "0x3fe8740c943957f7c7e49b69c851942715dce81ee9e11fa13a9e75f3b043b275", + "0x63a4274a62454a955f22d21f069d1b3e2d6363575c4a750e11f2995812f90e74", + "0x26226b71220e52c8b6dc7a178dc19aac15a573a7265aacf5f7759954cc98986d", + "0x8b61cc3180beb00631795f1d2531cca88f9b1f6d248d97fcd7c38f7dbcaa472f", + "0x2c57a59ab89e4d26331476c4fc2f51d8609c2167191df00c1e6c1b102d50b238", + "0x0eea66cf4a187c9066b1002188d73131ec21712cc37bc8a610d39751af2053a0", + "0x484d9e12fda7c62ed2ac5685bf489213458babc57739c7a651739b72173d5cab", + "0x54be9cbf8478fab1ee171958533d8194c347f2952378191af558d18c67a3f083", + "0x76db23e8fe6a0bcc24497fef09b70a4879162c01f6ee9db6092d0ddaefa1bf22", + "0x71b7ef900915a042344dab392a0c96c94fd26a13443b7c86a3d3c7fb2c4fde09", + "0x41edd24c863b7ef6ded961df4a1f92642b20b337b6f0267da52ec173cd5d1c1a", + "0x9a7d2ab4f886a1fa0670bccc1d0cd19c6ab1d36eb6e560b6e753a907701045fa", + "0x6b24298d39aa6a4a9b7246be34df95b2b3ed62879f0bb6bf7320ccd194e49cff", + "0xc758278db7c4d8fcc1b367e40f9e62f61cfed786afd77757fc82ad741809dd61", + "0x00d1f75cd2844b6a9ff0323c267d082e2986f9f379f90618e9d5996d23c3127c", + "0x9f2bd0e25c5c2646cb3574141d5d305244119114c187a65b9d46c2d88349ad02", + "0xdd3e6b5239ab18612b5e4370ecf9ccdddd5d9d7fc6d08613b4d83feb6e624f01", + "0x46b15b5f79e3dc84d1988401d6683118d10a5228646b89393f89b26f3b8451ec", + "0x6a01fa54920edae2d946e29438401b9ff9b11df405156109034dd77d4572ca1c", + "0x5c1d18405eb5448b2e4d965dba95c9596c18a5528d66feac65ffdf8a73a7e027", + "0x6cce0a2c290ff4f42d5ce1a5663d6864fc80c8fc5ce805e96573ee952471d8bb", + "0x5af72d2d4853ecb5df2058a7163d89053eb2378fa76f3e2539d81f868afa3e73", + "0xd16b79e0094d592eb44d2f00306f884ad2a47cbffcc0f4d94cb3205748f58de0", + "0xf06383d2928ca33d25144fd194acfe923aeaade6b302f0ab81a9fe049ff563c7", + "0x8742a439d4a1a64f630564a38699e9e1946f02b001bd78b752c81e11ff9eba4e", + "0x4be177b42e8a1a98577de605cf87a2ea7acdbed5cd8de619697fcbde95ef1fbe", + "0xd2a3167b6e6834021b486e8e7ff50a5fa9841406dbc83cc1cd38f387f7afc1ad", + "0x72e945227d6a2d84c9e75beb91769ef275b057214c750db1f93afb4f6db05c93", + "0xbea87d87cdfce069ea36a48eec1cbcb6cd3dad8ab69186b0172f20e9a8ced846", + "0x8095f9000ea0b8d582899a71e5c2e03fa4d02a66a99581b06a8a43613bfa36ae", + "0xdc7ba083f0d0df60defcbe63b71cca1b8c9f265f542951ceee26e2024c696ce4", + "0x598be2bd604f772f16518f5e804f7d4a1118ec38957117558e1f220c3933a566", + "0x76bd5909faff2ee24239660745b733c9494fb1a1eb76b4ca921652f776bb32bd", + "0xf2f74609fe6127d123a5ce0e4773c10ef329a1355cfe61aebace1fc8708a9483", + "0x4d477d6d1e5b607467590015707333bbed59fa0c75a6f3eedc263bc84e222bc6", + "0xa85e69a8c009a474229af418abae503f3ec73fe685c96f05019792089fc8881c", + "0xdd7b678b675656ae63190a5fdaa9a38ed9dcb75a4cedc7afcbc5b5edfcac4352", + "0x6447cba5a265235130c57c19bbbe2616227e41cc7b9a34d903e69740676c5bb8" + ], + "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncles": [] + }); + deepStrictEqual(hash, '0x29ac50ad0807f6755d615c27452f64887fc574c8378eb6c742671935910e2b27'); + }); + + test('Block number 44397064 (non-legacy)', { concurrency: true }, () => { + const { hash } = calculateZkSync({ + "baseFeePerGas": "0x2b275d0", + "difficulty": "0x0", + "extraData": "0x", + "gasLimit": "0x4000000000000", + "gasUsed": "0x369ab", + "l1BatchNumber": null, + "l1BatchTimestamp": null, + "logsBloom": "0x00000000000400000000000000000000000000000000020000001000040004000000001000000000000000000000000000000000000000000000008000000000000000000000040400001008000000080000000000000000000000000000080000000000020000002000000000000800000000000000000000000010000000000000000000000000000004000000000008000000000000000000000000000000200000080000100000000000000000000000000000000000000000000000000000000002008000000000000000000000000000000000000000000000000020000000000000000000000001000000000000000000000000000000000000020000", + "miner": "0x0000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0x2a57208", + "parentHash": "0x34f94a6acc50d437e021e433ec095d126df3f94453c170830a41c8c02c681a19", + "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sealFields": [], + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x0", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x66e91dac", + "totalDifficulty": "0x0", + "transactions": [ + "0xf78637acab3e71968e4857c1b7319d8a3024edb92d83b90fffde2ab994345756", + "0xd82e776565b4c285859a0902814ac23582c2f243c2b295db82429d9d366addda" + ], + "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncles": [] + }); + deepStrictEqual(hash, '0x6db9bcfdef7c293cc31465cd1dfcae27c9bd01fb6e2d8f246a544ea02b5f1527'); + }); }); \ No newline at end of file diff --git a/index.html b/index.html index 080d980..5a7119f 100644 --- a/index.html +++ b/index.html @@ -41,8 +41,8 @@
-

RLP-encoded header

- +

Encoded header

+

Block hash

@@ -53,7 +53,7 @@