]> git.ardis.lu - block-hash-calculator/commitdiff
Initial commit
authorArdis Lu <ardisglu@gmail.com>
Mon, 20 May 2024 05:32:05 +0000 (22:32 -0700)
committerArdis Lu <ardisglu@gmail.com>
Mon, 20 May 2024 05:32:05 +0000 (22:32 -0700)
README.md [new file with mode: 0644]
index.html [new file with mode: 0644]
vendor.js [new file with mode: 0644]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..eeb1441
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Block hash calculator
+
+Web app to visualize the calculation of a block's hash on Ethereum and EVM-like blockchains.
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..9e9651e
--- /dev/null
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="modulepreload" href="/vendor.js">
+  <link rel="icon" href="data:,">
+  <title>Block Hash Calculator</title>
+  <style>
+    body {
+      font-family: sans-serif;
+      margin-block: 0;
+      max-inline-size: min(100dvi, 1080px);
+      margin-inline: auto;
+      background-color: hsl(216deg 50% 93%);
+      color: hsl(216deg 50% 10%);
+    }
+
+    main {
+      margin-block: 16px;
+      margin-inline: 8px;
+    }
+
+    h1 {
+      font-size: 2rem;
+      font-weight: 300;
+      margin: 0;
+    }
+
+    form {
+      display: flex;
+      flex-direction: column;
+
+      & * {
+        font-size: 1rem;
+      }
+    }
+
+    label {
+      font-weight: 700;
+      margin-block-end: 4px;
+    }
+
+    input,
+    button {
+      padding: 8px;
+      border-radius: 4px;
+      border: 1px solid hsl(216deg 50% 85%);
+      outline: 2px solid transparent;
+      outline-offset: 1px;
+      margin-block-end: 8px;
+      transition: filter 300ms, border-color 300ms, outline-color 300ms;
+
+      &:hover {
+        transition: filter 150ms;
+        filter: brightness(102%);
+      }
+
+      &:focus-within {
+        transition: border-color 150ms, outline-color 150ms;
+        border-color: transparent;
+        outline-color: hsl(216deg 50% 40%);
+      }
+    }
+
+    button {
+      inline-size: fit-content;
+      cursor: pointer;
+    }
+
+    output {
+      display: none;
+      font-family: monospace;
+      transition: opacity 300ms;
+
+      @starting-style {
+        opacity: 0;
+      }
+    }
+
+    code {
+      white-space: pre-wrap;
+      word-break: break-all;
+    }
+  </style>
+</head>
+
+<body>
+  <main>
+    <h1>Block Hash Calculator</h1>
+    <p>Enter a block number and a JSON-RPC provider below to recalculate the block's hash from its header fields. Known supported networks: Ethereum, Polygon, BNB Chain, Optimism, and Base.</p>
+    <form>
+      <label for="num">Block number</label>
+      <input id="num" name="num" type="number" min="0" placeholder="12345678" required>
+
+      <label for="rpc">JSON-RPC</label>
+      <input id="rpc" name="rpc" type="url" placeholder="https://cloudflare-eth.com" value="https://cloudflare-eth.com" required>
+
+      <button>Calculate</button>
+
+      <output>
+        <section>
+          <h2>Block header</h2>
+          <code id="out-header"></code>
+        </section>
+        <section>
+          <h2>RLP-encoded header</h2>
+          <code id="out-rlp"></code>
+        </section>
+        <section>
+          <h2>Block hash</h2>
+          <code id="out-hash"></code>
+        </section>
+      </output>
+    </form>
+  </main>
+
+  <script type="module">
+    // ethereumjs/rlp v5.0.2
+    // noble/hashes/sha3 v1.4.0
+    import { RLP, keccak256 } from '/vendor.js';
+
+    // Uint8Array to hex string
+    function hex(array) {
+      return `0x${[...array].map(v => v.toString(16).padStart(2, '0')).join('')}`;
+    }
+
+    // Calculate the header, RLP-encoded header, and Keccak-256 hash of RLP
+    function calculate(block) {
+      // Header fields available at genesis
+      const header = [
+        block.parentHash,
+        block.sha3Uncles,
+        block.miner,
+        block.stateRoot,
+        block.transactionsRoot,
+        block.receiptsRoot,
+        block.logsBloom,
+        BigInt(block.difficulty),
+        BigInt(block.number),
+        BigInt(block.gasLimit),
+        BigInt(block.gasUsed),
+        BigInt(block.timestamp),
+        block.extraData,
+        block.mixHash,
+        BigInt(block.nonce)
+      ];
+
+      // London hard fork
+      // https://eips.ethereum.org/EIPS/eip-1559#block-hash-changing
+      if (block?.baseFeePerGas) {
+        header.push(BigInt(block.baseFeePerGas));
+      }
+
+      // Paris hard fork
+      // https://eips.ethereum.org/EIPS/eip-3675#block-structure
+      if (block.nonce === '0x0000000000000000') {
+        header[14] = '0x0000000000000000'; // MUST be string and not a number
+      }
+
+      // Shapella hard fork
+      // https://eips.ethereum.org/EIPS/eip-6465#execution-block-header-changes
+      if (block?.withdrawalsRoot) {
+        header.push(block.withdrawalsRoot);
+      }
+
+      // Dencun hard fork
+      // https://eips.ethereum.org/EIPS/eip-4844#header-extension
+      if (block?.blobGasUsed) {
+        header.push(
+          BigInt(block.blobGasUsed),
+          BigInt(block.excessBlobGas),
+          block.parentBeaconBlockRoot
+        );
+      }
+
+      const rlp = RLP.encode(header);
+      const hash = keccak256(rlp);
+
+      return {
+        header,
+        rlp: hex(rlp),
+        hash: hex(hash)
+      };
+    }
+
+    document.querySelector('form').addEventListener('submit', async e => {
+      e.preventDefault();
+
+      const { num, rpc } = Object.fromEntries(new FormData(e.target));
+      const response = await fetch(rpc, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          id: 1,
+          method: 'eth_getBlockByNumber',
+          params: [`0x${BigInt(num).toString(16)}`, false]
+        })
+      }).then(r => r.json());
+
+      const block = response?.result;
+      if (block === undefined) {
+        return;
+      }
+      const { header, rlp, hash } = calculate(block);
+
+      document.querySelector('output').style.setProperty('display', 'unset');
+      document.querySelector('#out-header').textContent = `[\n  ${header.join(',\n  ')}\n]`;
+      document.querySelector('#out-rlp').textContent = rlp;
+      document.querySelector('#out-hash').textContent = hash;
+    });
+  </script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/vendor.js b/vendor.js
new file mode 100644 (file)
index 0000000..a53a2ee
--- /dev/null
+++ b/vendor.js
@@ -0,0 +1,6 @@
+function m(t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`positive integer expected, not ${t}`)}function Y(t){return t instanceof Uint8Array||t!=null&&typeof t=="object"&&t.constructor.name==="Uint8Array"}function h(t,...e){if(!Y(t))throw new Error("Uint8Array expected");if(e.length>0&&!e.includes(t.length))throw new Error(`Uint8Array expected of length ${e}, not of length=${t.length}`)}function L(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function I(t,e){h(t);let n=e.outputLen;if(t.length<n)throw new Error(`digestInto() expects output buffer of length at least ${n}`)}var g=BigInt(4294967295),O=BigInt(32);function Z(t,e=!1){return e?{h:Number(t&g),l:Number(t>>O&g)}:{h:Number(t>>O&g)|0,l:Number(t&g)|0}}function U(t,e=!1){let n=new Uint32Array(t.length),r=new Uint32Array(t.length);for(let o=0;o<t.length;o++){let{h:s,l:i}=Z(t[o],e);[n[o],r[o]]=[s,i]}return[n,r]}var S=(t,e,n)=>t<<n|e>>>32-n,H=(t,e,n)=>e<<n|t>>>32-n,T=(t,e,n)=>e<<n-32|t>>>64-n,R=(t,e,n)=>t<<n-32|e>>>64-n;var P=t=>new Uint32Array(t.buffer,t.byteOffset,Math.floor(t.byteLength/4));var A=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68,tt=t=>t<<24&4278190080|t<<8&16711680|t>>>8&65280|t>>>24&255;function k(t){for(let e=0;e<t.length;e++)t[e]=tt(t[e])}function et(t){if(typeof t!="string")throw new Error(`utf8ToBytes expected string, got ${typeof t}`);return new Uint8Array(new TextEncoder().encode(t))}function w(t){return typeof t=="string"&&(t=et(t)),h(t),t}var y=class{clone(){return this._cloneInto()}},Lt={}.toString;function F(t){let e=r=>t().update(w(r)).digest(),n=t();return e.outputLen=n.outputLen,e.blockLen=n.blockLen,e.create=()=>t(),e}function N(t){let e=(r,o)=>t(o).update(w(r)).digest(),n=t({});return e.outputLen=n.outputLen,e.blockLen=n.blockLen,e.create=r=>t(r),e}var X=[],C=[],V=[],nt=BigInt(0),d=BigInt(1),rt=BigInt(2),ot=BigInt(7),it=BigInt(256),st=BigInt(113);for(let t=0,e=d,n=1,r=0;t<24;t++){[n,r]=[r,(2*n+3*r)%5],X.push(2*(5*r+n)),C.push((t+1)*(t+2)/2%64);let o=nt;for(let s=0;s<7;s++)e=(e<<d^(e>>ot)*st)%it,e&rt&&(o^=d<<(d<<BigInt(s))-d);V.push(o)}var[ct,ft]=U(V,!0),j=(t,e,n)=>n>32?T(t,e,n):S(t,e,n),$=(t,e,n)=>n>32?R(t,e,n):H(t,e,n);function ut(t,e=24){let n=new Uint32Array(10);for(let r=24-e;r<24;r++){for(let i=0;i<10;i++)n[i]=t[i]^t[i+10]^t[i+20]^t[i+30]^t[i+40];for(let i=0;i<10;i+=2){let c=(i+8)%10,f=(i+2)%10,p=n[f],a=n[f+1],K=j(p,a,1)^n[c],Q=$(p,a,1)^n[c+1];for(let x=0;x<50;x+=10)t[i+x]^=K,t[i+x+1]^=Q}let o=t[2],s=t[3];for(let i=0;i<24;i++){let c=C[i],f=j(o,s,c),p=$(o,s,c),a=X[i];o=t[a],s=t[a+1],t[a]=f,t[a+1]=p}for(let i=0;i<50;i+=10){for(let c=0;c<10;c++)n[c]=t[i+c];for(let c=0;c<10;c++)t[i+c]^=~n[(c+2)%10]&n[(c+4)%10]}t[0]^=ct[r],t[1]^=ft[r]}n.fill(0)}var b=class t extends y{constructor(e,n,r,o=!1,s=24){if(super(),this.blockLen=e,this.suffix=n,this.outputLen=r,this.enableXOF=o,this.rounds=s,this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,m(r),0>=this.blockLen||this.blockLen>=200)throw new Error("Sha3 supports only keccak-f1600 function");this.state=new Uint8Array(200),this.state32=P(this.state)}keccak(){A||k(this.state32),ut(this.state32,this.rounds),A||k(this.state32),this.posOut=0,this.pos=0}update(e){L(this);let{blockLen:n,state:r}=this;e=w(e);let o=e.length;for(let s=0;s<o;){let i=Math.min(n-this.pos,o-s);for(let c=0;c<i;c++)r[this.pos++]^=e[s++];this.pos===n&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;let{state:e,suffix:n,pos:r,blockLen:o}=this;e[r]^=n,n&128&&r===o-1&&this.keccak(),e[o-1]^=128,this.keccak()}writeInto(e){L(this,!1),h(e),this.finish();let n=this.state,{blockLen:r}=this;for(let o=0,s=e.length;o<s;){this.posOut>=r&&this.keccak();let i=Math.min(r-this.posOut,s-o);e.set(n.subarray(this.posOut,this.posOut+i),o),this.posOut+=i,o+=i}return e}xofInto(e){if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(e)}xof(e){return m(e),this.xofInto(new Uint8Array(e))}digestInto(e){if(I(e,this),this.finished)throw new Error("digest() was already called");return this.writeInto(e),this.destroy(),e}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,this.state.fill(0)}_cloneInto(e){let{blockLen:n,suffix:r,outputLen:o,rounds:s,enableXOF:i}=this;return e||(e=new t(n,r,o,i,s)),e.state32.set(this.state32),e.pos=this.pos,e.posOut=this.posOut,e.finished=this.finished,e.rounds=s,e.suffix=r,e.outputLen=o,e.enableXOF=i,e.destroyed=this.destroyed,e}},u=(t,e,n)=>F(()=>new b(e,t,n)),Et=u(6,144,224/8),It=u(6,136,256/8),Ot=u(6,104,384/8),Ut=u(6,72,512/8),St=u(1,144,224/8),at=u(1,136,256/8),Ht=u(1,104,384/8),Tt=u(1,72,512/8),v=(t,e,n)=>N((r={})=>new b(e,t,r.dkLen===void 0?n:r.dkLen,!0)),Rt=v(31,168,128/8),Pt=v(31,136,256/8);function z(t){if(Array.isArray(t)){let n=[],r=0;for(let o=0;o<t.length;o++){let s=z(t[o]);n.push(s),r+=s.length}return W(D(r,192),...n)}let e=J(t);return e.length===1&&e[0]<128?e:W(D(e.length,128),e)}function l(t,e,n){if(n>t.length)throw new Error("invalid RLP (safeSlice): end slice of Uint8Array out-of-bounds");return t.slice(e,n)}function M(t){if(t[0]===0)throw new Error("invalid RLP: extra zeros");return q(dt(t))}function D(t,e){if(t<56)return Uint8Array.from([t+e]);let n=E(t),r=n.length/2,o=E(e+55+r);return Uint8Array.from(B(o+n))}function lt(t,e=!1){if(typeof t>"u"||t===null||t.length===0)return Uint8Array.from([]);let n=J(t),r=_(n);if(e)return{data:r.data,remainder:r.remainder.slice()};if(r.remainder.length!==0)throw new Error("invalid RLP: remainder must be zero");return r.data}function _(t){let e,n,r,o,s,i=[],c=t[0];if(c<=127)return{data:t.slice(0,1),remainder:t.subarray(1)};if(c<=183){if(e=c-127,c===128?r=Uint8Array.from([]):r=l(t,1,e),e===2&&r[0]<128)throw new Error("invalid RLP encoding: invalid prefix, single byte < 0x80 are not prefixed");return{data:r,remainder:t.subarray(e)}}else if(c<=191){if(n=c-182,t.length-1<n)throw new Error("invalid RLP: not enough bytes for string length");if(e=M(l(t,1,n)),e<=55)throw new Error("invalid RLP: expected string length to be greater than 55");return r=l(t,n,e+n),{data:r,remainder:t.subarray(e+n)}}else if(c<=247){for(e=c-191,o=l(t,1,e);o.length;)s=_(o),i.push(s.data),o=s.remainder;return{data:i,remainder:t.subarray(e)}}else{if(n=c-246,e=M(l(t,1,n)),e<56)throw new Error("invalid RLP: encoded list too short");let f=n+e;if(f>t.length)throw new Error("invalid RLP: total length is larger than the data");for(o=l(t,n,f);o.length;)s=_(o),i.push(s.data),o=s.remainder;return{data:i,remainder:t.subarray(f)}}}var ht=Array.from({length:256},(t,e)=>e.toString(16).padStart(2,"0"));function dt(t){let e="";for(let n=0;n<t.length;n++)e+=ht[t[n]];return e}function q(t){let e=Number.parseInt(t,16);if(Number.isNaN(e))throw new Error("Invalid byte sequence");return e}function B(t){if(typeof t!="string")throw new TypeError("hexToBytes: expected string, got "+typeof t);if(t.length%2)throw new Error("hexToBytes: received invalid unpadded hex");let e=new Uint8Array(t.length/2);for(let n=0;n<e.length;n++){let r=n*2;e[n]=q(t.slice(r,r+2))}return e}function W(...t){if(t.length===1)return t[0];let e=t.reduce((r,o)=>r+o.length,0),n=new Uint8Array(e);for(let r=0,o=0;r<t.length;r++){let s=t[r];n.set(s,o),o+=s.length}return n}function pt(t){return new TextEncoder().encode(t)}function E(t){if(t<0)throw new Error("Invalid integer as argument, must be unsigned!");let e=t.toString(16);return e.length%2?`0${e}`:e}function xt(t){return t.length%2?`0${t}`:t}function G(t){return t.length>=2&&t[0]==="0"&&t[1]==="x"}function gt(t){return typeof t!="string"?t:G(t)?t.slice(2):t}function J(t){if(t instanceof Uint8Array)return t;if(typeof t=="string")return G(t)?B(xt(gt(t))):pt(t);if(typeof t=="number"||typeof t=="bigint")return t?B(E(t)):Uint8Array.from([]);if(t==null)return Uint8Array.from([]);throw new Error("toBytes: received unsupported type "+typeof t)}var yt={encode:z,decode:lt};export{yt as RLP,at as keccak256};
+/*! Bundled license information:
+
+@noble/hashes/esm/utils.js:
+  (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
+*/
This page took 0.047974 seconds and 4 git commands to generate.