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
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
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 }, () => {
});
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