]> git.ardis.lu - block-hash-calculator/commitdiff
Add initial support for zkSync Era
authorArdis Lu <ardisglu@gmail.com>
Tue, 17 Sep 2024 06:35:29 +0000 (23:35 -0700)
committerArdis Lu <ardisglu@gmail.com>
Tue, 17 Sep 2024 06:35:29 +0000 (23:35 -0700)
calculate.js
calculate.test.js
index.html

index ae0ecbce3f6ca4314f955d6b87d09b68c47d6ba2..df20fd737b484b2dbd6b7e311f9ca504e17ec455 100644 (file)
@@ -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
index f1ca8a17c7cc6aacedabcf2d95982b61b9c6ee0c..81ca57ef1562afc22096ee2267e992607df61b96 100644 (file)
@@ -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
index 080d980378a74383f9f7055221a9225e41836205..5a7119f57539703bcd46a169fc072751bb464ba3 100644 (file)
@@ -41,8 +41,8 @@
           <code id="out-header"></code>
         </section>
         <section>
-          <h2>RLP-encoded header</h2>
-          <code id="out-rlp"></code>
+          <h2>Encoded header</h2>
+          <code id="out-encoded"></code>
         </section>
         <section>
           <h2>Block hash</h2>
@@ -53,7 +53,7 @@
   </main>
 
   <script type="module">
-    import { calculate } from './calculate.js';
+    import { calculate, calculateZkSync } from './calculate.js';
 
     document.querySelector('form').addEventListener('submit', async e => {
       e.preventDefault();
 
       const block = response?.result;
       if (block !== undefined) {
-        const { header, rlp, hash } = calculate(block);
+        const { header, encoded, hash } = calculate(block);
 
-        document.querySelector('#out-header').textContent = `[\n  ${header.join(',\n  ')}\n]`;
-        document.querySelector('#out-rlp').textContent = rlp;
-        document.querySelector('#out-hash').textContent = hash;
+        if (hash !== block.hash) { // TODO: use a better heuristic to determine if it's zkSync
+          const { header, encoded, hash } = calculateZkSync(block);
+          document.querySelector('#out-header').textContent = `[\n  ${header.join(',\n  ')}\n]`;
+          document.querySelector('#out-encoded').textContent = encoded;
+          document.querySelector('#out-hash').textContent = hash;
+        }
+        else {
+          document.querySelector('#out-header').textContent = `[\n  ${header.join(',\n  ')}\n]`;
+          document.querySelector('#out-encoded').textContent = encoded;
+          document.querySelector('#out-hash').textContent = hash;
+        }
       }
       else {
         document.querySelector('#out-header').textContent = `Unable to retrieve block header. Error: ${response?.message ?? 'Other'}`;
-        document.querySelector('#out-rlp').textContent = 'N/A';
+        document.querySelector('#out-encoded').textContent = 'N/A';
         document.querySelector('#out-hash').textContent = 'N/A';
       }
       document.querySelector('output').style.setProperty('display', 'unset');
This page took 0.047459 seconds and 4 git commands to generate.