diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 87eb3de3..6a5456b4 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -21,7 +21,9 @@ WORKDIR /srv/explorer/source SHELL ["/bin/bash", "-c"] RUN source /root/.nvm/nvm.sh \ - && npm install && (cd prerender-server && npm run dist) \ + && npm install \ + && npm test \ + && (cd prerender-server && npm run dist) \ && DEST=/srv/explorer/static/bitcoin-mainnet \ npm run dist -- bitcoin-mainnet \ && DEST=/srv/explorer/static/bitcoin-testnet \ diff --git a/package.json b/package.json index 65925e7a..e90a926f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "prerender-server": "cd prerender-server && npm start", "postinstall": "(cd client && npm install) && (cd prerender-server && npm install)", "dist": "./build.sh", + "test": "node --require @babel/register --test test/*.test.js", "l10n:fetch": "./lang/util/fetch-transifex.sh", "l10n:update-strings": "./lang/util/update-strings.sh" }, @@ -13,6 +14,7 @@ "license": "MIT", "browserslist": "> 0.5%", "devDependencies": { + "@babel/register": "^7.12.10", "browserify-middleware": "^8.1.1", "express": "^4.17.1", "glob": "^7.1.6", diff --git a/test/block-signatures.test.js b/test/block-signatures.test.js new file mode 100644 index 00000000..c1f5ab84 --- /dev/null +++ b/test/block-signatures.test.js @@ -0,0 +1,52 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const { parseBlockSignatures } = require("../client/src/lib/block-signatures"); + +test("parses a signblock witness", () => { + const block = { + ext: { + signblock_witness: [ + [], + [0x01], + [0x02], + [0x52, 0x53, 0xae], + ], + }, + }; + + assert.deepEqual(parseBlockSignatures(block), { + requiredSignatures: 2, + totalSigners: 3, + signatureCount: 2, + source: "signblock_witness", + }); +}); + +test("parses a legacy block proof", () => { + const signature = `09${"00".repeat(9)}`; + const block = { + proof: { + challenge: "5253ae", + solution: `00${signature}${signature}`, + }, + }; + + assert.deepEqual(parseBlockSignatures(block), { + requiredSignatures: 2, + totalSigners: 3, + signatureCount: 2, + source: "challenge", + }); +}); + +test("returns source-tagged errors for malformed signature data", () => { + const result = parseBlockSignatures({ + ext: { + signblock_witness: [[0x00], [0x51, 0x51, 0xae]], + }, + }); + + assert.equal(result.error.source, "signblock_witness"); + assert.match(result.error.message, /dummy must be empty/); +}); diff --git a/test/deduce-blinded.test.js b/test/deduce-blinded.test.js new file mode 100644 index 00000000..93c4354a --- /dev/null +++ b/test/deduce-blinded.test.js @@ -0,0 +1,57 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const { deduceBlinded } = require("../client/src/lib/deduce-blinded"); + +test("deduces a single blinded output", () => { + const tx = { + vin: [ + { prevout: { asset: "asset-a", value: 7 } }, + { prevout: { asset: "asset-a", value: 4 } }, + ], + vout: [ + { asset: "asset-a", value: 6 }, + { asset: null, value: null }, + ], + }; + + deduceBlinded(tx); + + assert.deepEqual(tx.vout[1], { asset: "asset-a", value: 5 }); + + tx.vin[0].prevout.value = 100; + deduceBlinded(tx); + assert.deepEqual(tx.vout[1], { asset: "asset-a", value: 5 }); +}); + +test("deduces a single blinded input using bigint amounts", () => { + const tx = { + vin: [ + { prevout: { asset: null, value: null } }, + { prevout: { asset: "asset-a", value: 3n } }, + ], + vout: [{ asset: "asset-a", value: 8n }], + }; + + deduceBlinded(tx); + + assert.deepEqual(tx.vin[0].prevout, { + asset: "asset-a", + value: 5n, + }); +}); + +test("rejects inconsistent balances instead of guessing a blinded value", () => { + const tx = { + vin: [ + { prevout: { asset: "asset-a", value: 10 } }, + { prevout: { asset: "asset-b", value: 5 } }, + ], + vout: [{ asset: null, value: null }], + }; + + assert.throws( + () => deduceBlinded(tx), + /unexpected remainder while deducing blinded tx/, + ); +}); diff --git a/test/fees.test.js b/test/fees.test.js new file mode 100644 index 00000000..d4e66ca0 --- /dev/null +++ b/test/fees.test.js @@ -0,0 +1,36 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const { + feerateCutoff, + getConfEstimate, +} = require("../client/src/lib/fees"); + +test("calculates fee cutoff at block size boundaries", () => { + assert.equal( + feerateCutoff([ + [10, 500000], + [5, 499999], + ]), + 0, + ); + assert.equal( + feerateCutoff([ + [10, 500000], + [5, 500000], + ]), + 10, + ); + assert.equal(feerateCutoff([[10, 1000000]]), null); +}); + +test("selects a confirmation target by numeric order", () => { + const estimates = { + 25: 1, + 10: 5, + 2: 10, + }; + + assert.equal(getConfEstimate(estimates, 6), "10"); + assert.equal(getConfEstimate(estimates, 0.5), -1); +}); diff --git a/test/privacy-analysis.test.js b/test/privacy-analysis.test.js new file mode 100644 index 00000000..eaabe24c --- /dev/null +++ b/test/privacy-analysis.test.js @@ -0,0 +1,52 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const getPrivacyAnalysis = + require("../client/src/lib/privacy-analysis").default; + +test("skips privacy analysis for coinbase transactions", () => { + const tx = { + vin: [{ is_coinbase: true }], + vout: [], + }; + + assert.deepEqual(getPrivacyAnalysis(tx), []); +}); + +test("detects equal-output coinjoins only with explicit amounts", () => { + const vin = [ + { + prevout: { + value: 2000, + scriptpubkey: "input-a", + scriptpubkey_type: "v0_p2wpkh", + }, + }, + { + prevout: { + value: 2000, + scriptpubkey: "input-b", + scriptpubkey_type: "v0_p2wpkh", + }, + }, + ]; + const vout = [1000, 1000, 900, 800].map((value, index) => ({ + value, + scriptpubkey: `output-${index}`, + scriptpubkey_type: "v0_p2wpkh", + })); + + assert.deepEqual( + getPrivacyAnalysis({ vin, vout }), + ["coinjoin-equal-outputs"], + ); + + const confidentialVin = [ + { ...vin[0], prevout: { ...vin[0].prevout, value: null } }, + vin[1], + ]; + assert.deepEqual( + getPrivacyAnalysis({ vin: confidentialVin, vout }), + [], + ); +});