btcnotar 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,174 +1,240 @@
1
1
  # btcnotar
2
2
 
3
- Batch Bitcoin notarization for Node.js.
3
+ **Prove that a text or a file existed at a certain time — using the Bitcoin blockchain.**
4
4
 
5
- `btcnotar` collects document hashes, combines them into a **Merkle tree**,
6
- and anchors only the tree's root on the Bitcoin blockchain via a single
7
- `OP_RETURN` transaction — automatically, on a schedule (once a day by
8
- default). Each individual hash gets back a small **inclusion proof** that
9
- lets anyone later prove — unambiguously, against the live chain, down to
10
- the transaction id, block and date — that their document was notarized,
11
- without needing the rest of the batch.
5
+ - **Free.** No Bitcoin, no wallet, no key. The public server pays the fee.
6
+ - **Private.** Only the SHA-256 hash is sent. Your file never leaves your computer.
7
+ - **Trustless.** `verify()` checks the proof directly on the blockchain, not on our server.
12
8
 
13
- ## Why batching
9
+ ```bash
10
+ npm install btcnotar
11
+ ```
14
12
 
15
- Anchoring one hash per transaction is expensive and doesn't scale. Batching
16
- thousands of hashes into one Merkle root per settlement means:
13
+ ---
17
14
 
18
- - **One on-chain transaction settles an unlimited number of documents.**
19
- - **Cheap**: fees are paid once per batch, not per document.
20
- - **Reversible / verifiable**: given only your original hash and its proof,
21
- you can recompute the Merkle root and confirm it matches what's actually
22
- on-chain — no trust in the notary's database required.
15
+ ## Easy examples
23
16
 
24
- ## Install
17
+ All examples use the free public server. Save them as `.mjs` files and run with `node`.
25
18
 
26
- ```bash
27
- npm install btcnotar
19
+ ### 1. Notarize a text
20
+
21
+ ```js
22
+ // notarize-text.mjs
23
+ import { BTCNotarClient, sha256Hex } from "btcnotar";
24
+
25
+ const client = new BTCNotarClient();
26
+ const hash = sha256Hex("Hello, Bitcoin!");
27
+
28
+ const result = await client.submit(hash);
29
+ console.log(result);
30
+ // { status: "pending", hash: "...", nextSettleAt: "2026-10-25T15:00:00.000Z" }
28
31
  ```
29
32
 
30
- ## Quick start
33
+ `pending` means: your hash is in the queue. It goes on the blockchain at `nextSettleAt` (about 1 hour).
34
+
35
+ ### 2. Check it later
31
36
 
32
37
  ```js
33
- import { BTCNotar, FileStore } from "btcnotar";
38
+ // check-text.mjs
39
+ import { BTCNotarClient, sha256Hex } from "btcnotar";
40
+
41
+ const client = new BTCNotarClient();
42
+ const result = await client.verify(sha256Hex("Hello, Bitcoin!"));
43
+
44
+ if (result.valid) {
45
+ console.log("On the blockchain!");
46
+ console.log("Date: ", result.date);
47
+ console.log("Block:", result.blockHeight);
48
+ console.log("Tx: ", `https://mempool.space/tx/${result.txid}`);
49
+ } else {
50
+ console.log("Not yet:", result.reason); // "pending" = still waiting
51
+ }
52
+ ```
34
53
 
35
- const notary = new BTCNotar({
36
- wif: process.env.BTCNOTAR_WIF, // Bitcoin private key (WIF) that funds/signs anchor txs
37
- network: "mainnet", // or "testnet"
38
- store: new FileStore({ filePath: "./.btcnotar/store.json" }),
39
- intervalMs: 24 * 60 * 60 * 1000, // settle once a day (default)
40
- });
54
+ ### 3. Notarize a file
55
+
56
+ ```js
57
+ // notarize-file.mjs
58
+ import fs from "node:fs";
59
+ import { BTCNotarClient, sha256Hex } from "btcnotar";
41
60
 
42
- // 1. Queue hashes as documents come in (e.g. sha256 of a file)
43
- await notary.addHash("2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881");
61
+ const client = new BTCNotarClient();
62
+ const hash = sha256Hex(fs.readFileSync("contract.pdf"));
44
63
 
45
- // 2. Start automatic daily settlement (builds the Merkle tree + broadcasts
46
- // one OP_RETURN transaction anchoring the root)
47
- notary.start();
64
+ console.log("SHA-256:", hash);
65
+ console.log(await client.submit(hash));
66
+ ```
48
67
 
49
- // ... later, anywhere, even in a different process that only has the hash ...
68
+ Keep the file **exactly as it is**. If one byte changes, the hash changes and the proof will not match.
50
69
 
51
- const proof = await notary.getProof(hash);
52
- // { hash, merkleRoot, proof, txid, batchId, blockHeight, blockHash, blockTime, ... }
70
+ ### 4. Many texts, save results, check later
53
71
 
54
- const result = await notary.verify(hash);
55
- // {
56
- // valid: true,
57
- // confirmed: true,
58
- // txid: "…",
59
- // blockHeight: 912345,
60
- // blockHash: "…",
61
- // date: "2026-09-10T00:00:00.000Z",
62
- // merkleRoot: "…",
63
- // }
72
+ ```js
73
+ // many.mjs
74
+ import fs from "node:fs";
75
+ import { BTCNotarClient, sha256Hex } from "btcnotar";
76
+
77
+ const client = new BTCNotarClient();
78
+ const texts = ["first text", "second text", "third text"];
79
+
80
+ const results = [];
81
+ for (const text of texts) {
82
+ const hash = sha256Hex(text);
83
+ const r = await client.submit(hash);
84
+ results.push({ text, hash, status: r.status, nextSettleAt: r.nextSettleAt });
85
+ }
86
+
87
+ fs.writeFileSync("results.json", JSON.stringify(results, null, 2));
88
+ console.log("Saved to results.json");
64
89
  ```
65
90
 
66
- `verify()` does two independent checks before it reports `valid: true`:
91
+ ```js
92
+ // check-many.mjs
93
+ import fs from "node:fs";
94
+ import { BTCNotarClient } from "btcnotar";
67
95
 
68
- 1. **Cryptographic**: recomputes the Merkle root from the hash + its proof
69
- and checks it equals the stored root.
70
- 2. **On-chain**: fetches the anchoring transaction live from the blockchain
71
- and confirms its `OP_RETURN` output actually carries that same root.
96
+ const client = new BTCNotarClient();
97
+ const results = JSON.parse(fs.readFileSync("results.json", "utf8"));
72
98
 
73
- Only when both agree is the hash reported as anchored — together with the
74
- transaction id, block hash/height and date, so "is this document on the
75
- blockchain, and when/where" has one unambiguous answer.
99
+ for (const item of results) {
100
+ const r = await client.verify(item.hash);
101
+ console.log(item.text, "->", r.valid ? `YES, block ${r.blockHeight}, ${r.date}` : `not yet (${r.reason})`);
102
+ }
103
+ ```
76
104
 
77
- ## API
105
+ All texts sent in the same hour go into **one** Bitcoin transaction.
78
106
 
79
- ### `new BTCNotar(options)`
80
-
81
- | option | required | default | description |
82
- |---------------|----------|---------------------|-------------|
83
- | `wif` | yes | — | Bitcoin private key (WIF) used to fund and sign anchor transactions |
84
- | `network` | no | `"mainnet"` | `"mainnet"` or `"testnet"` |
85
- | `store` | no | `new MemoryStore()` | pending hashes + settled batches storage (see below) |
86
- | `provider` | no | `new MempoolProvider()` | chain data source (UTXOs, fees, broadcast, tx lookup) |
87
- | `feeRateSatVb`| no | fetched live | fixed fee rate override (sat/vB) |
88
- | `intervalMs` | no | `86400000` (24h) | used by `start()`: a batch is anchored once the oldest pending hash has waited this long (no tx while the queue is empty) |
89
-
90
- ### Instance methods
91
-
92
- - `addHash(hashHex)` — queue a hex-encoded hash for the next settlement.
93
- - `pendingCount()` — number of hashes waiting to be settled.
94
- - `settle()` — build a Merkle tree over everything pending and anchor its
95
- root in one `OP_RETURN` transaction right now. Returns the batch record
96
- (`{ batchId, merkleRoot, txid, hashes, proofs, ... }`), or `null` if
97
- nothing was pending.
98
- - `start({ intervalMs })` / `stop()` — automatic settlement: checks every minute and anchors when the oldest pending hash is `intervalMs` old. Survives restarts (uses the stored `addedAt`).
99
- - `settleIfDue()` — the check `start()` runs; anchors only if something has waited `intervalMs`.
100
- - `anchorNow(hash)` — anchor one hash immediately (together with anything already queued) and return its proof. If the hash is already anchored, returns the existing proof without a new transaction.
101
- - `getProof(hashHex)` — look up the stored proof + anchor info for a hash.
102
- - `verify(hashHex)` — cryptographically and on-chain verification (see above).
103
- - `refreshStatus(batchId)` — re-fetch a batch's transaction and update its
104
- confirmation / block info.
105
-
106
- ### `MerkleTree`
107
-
108
- The underlying Merkle tree, usable standalone:
107
+ ### 5. Command line (no code)
109
108
 
110
- ```js
111
- import { MerkleTree } from "btcnotar";
109
+ ```bash
110
+ npx btcnotar submit "Hello, Bitcoin!"
111
+ npx btcnotar check "Hello, Bitcoin!"
112
+ ```
113
+
114
+ You can give a text or a SHA-256 hash (64 hex characters).
115
+
116
+ ---
117
+
118
+ ## How long does it take?
119
+
120
+ | Step | Time |
121
+ |---|---|
122
+ | `submit()` | instant — status `pending` |
123
+ | Hash goes into a Bitcoin transaction | at `nextSettleAt` (about every hour) |
124
+ | Transaction is in a block | about 10 minutes later |
125
+
126
+ After the transaction is sent, `verify()` returns `valid: true`.
127
+ `confirmed: false` means it is still waiting for a block. `confirmed: true` means it is in a block.
112
128
 
113
- const tree = new MerkleTree([hashA, hashB, hashC]);
114
- tree.root; // hex string
115
- const proof = tree.getProof(0); // inclusion proof for hashA
116
- MerkleTree.verifyProof(hashA, proof, tree.root); // true
129
+ ## What `verify()` returns
130
+
131
+ ```js
132
+ { valid: true, confirmed: true, txid: "...", blockHeight: 920123, date: "2026-10-25T15:08:12.000Z", merkleRoot: "..." }
133
+ { valid: false, reason: "pending", nextSettleAt: "..." } // waiting in the queue
134
+ { valid: false, reason: "unknown-hash" } // never sent
135
+ { valid: false, reason: "merkle-proof-mismatch" } // proof is wrong
136
+ { valid: false, reason: "on-chain-data-mismatch" } // blockchain has different data
117
137
  ```
118
138
 
119
- Leaves are domain-separated from internal nodes (distinct hash prefixes),
120
- and an unpaired node at the end of a level is carried up unchanged rather
121
- than duplicated — this avoids both the classic leaf/node confusion forgery
122
- and the Bitcoin-style duplicate-leaf ambiguity (CVE-2012-2459).
139
+ ## Why you don't need to trust the server
140
+
141
+ `verify()` downloads the proof from the server, then:
142
+
143
+ 1. rebuilds the Merkle root from your hash and the proof, and
144
+ 2. reads the real Bitcoin transaction from [mempool.space](https://mempool.space) and checks that its `OP_RETURN` contains that root.
145
+
146
+ If the server lies, the check fails.
147
+
148
+ ---
149
+
150
+ ## Run your own notary (with your own Bitcoin)
123
151
 
124
- ### Storage
152
+ If you want your own server, use `BTCNotar` with your own key (WIF) and a funded address.
125
153
 
126
- `MemoryStore` (default, non-persistent) and `FileStore` (JSON file on disk)
127
- are included and share the same interface: `addPending`, `listPending`,
128
- `clearPending`, `saveBatch`, `getBatch`, `findBatchByHash`, `listBatches`.
129
- Implement the same interface against a real database for multi-process or
130
- production use.
154
+ ```js
155
+ import { BTCNotar, FileStore, sha256Hex } from "btcnotar";
131
156
 
132
- ### Chain provider
157
+ const notary = new BTCNotar({
158
+ wif: process.env.BTCNOTAR_WIF, // your private key (WIF)
159
+ store: new FileStore({ filePath: "./store.json" }), // keeps queue and proofs
160
+ intervalMs: 60 * 60 * 1000, // anchor every hour (if queue is not empty)
161
+ });
133
162
 
134
- `MempoolProvider` talks to the public [mempool.space](https://mempool.space)
135
- REST API for UTXOs, fee estimation, broadcasting and transaction lookup.
136
- Supply your own object implementing `fetchUtxos`, `fetchFeeRateSatVb`,
137
- `broadcastTx`, `fetchTransaction` to use a different backend (e.g. a local
138
- `bitcoind` or Electrum server).
163
+ console.log("Fund this address:", notary.address);
139
164
 
140
- ## CLI
165
+ await notary.addHash(sha256Hex("my document")); // add to queue
166
+ notary.start(); // anchors automatically
141
167
 
142
- The package also installs a `btcnotar` command:
168
+ // or right now:
169
+ const proof = await notary.anchorNow(sha256Hex("urgent document"));
170
+ console.log(proof.txid);
171
+
172
+ // check:
173
+ console.log(await notary.verify(sha256Hex("my document")));
174
+ ```
175
+
176
+ Command line for your own notary:
143
177
 
144
178
  ```bash
145
- export BTCNOTAR_WIF=... # required
146
- export BTCNOTAR_NETWORK=mainnet
147
- export BTCNOTAR_STORE=./.btcnotar/store.json
148
-
149
- npx btcnotar add <hash>
150
- npx btcnotar settle
151
- npx btcnotar proof <hash>
152
- npx btcnotar verify <hash>
153
- npx btcnotar address
179
+ export BTCNOTAR_WIF=... # required
180
+ npx btcnotar address # address to fund
181
+ npx btcnotar add <hash> # add to queue
182
+ npx btcnotar settle # send the transaction now
183
+ npx btcnotar verify <hash> # check on the blockchain
154
184
  ```
155
185
 
156
- `btcnotar settle` is handy if you'd rather trigger settlement from an
157
- external scheduler (system cron, a serverless cron trigger) instead of the
158
- built-in `start()` interval.
186
+ ---
159
187
 
160
- ## How anchoring works on-chain
188
+ ## API
161
189
 
162
- Each settlement broadcasts one transaction with a zero-value `OP_RETURN`
163
- output carrying:
190
+ ### `new BTCNotarClient(options?)` — free public server
191
+
192
+ | option | default | description |
193
+ |---|---|---|
194
+ | `serverUrl` | public server | your own server URL |
195
+ | `network` | `"mainnet"` | `"mainnet"` or `"testnet"` |
196
+ | `provider` | `MempoolProvider` | where `verify()` reads the blockchain |
197
+
198
+ - `submit(hash)` — add a hash to the queue → `{ status, hash, addedAt, nextSettleAt }`
199
+ - `verify(hash)` — check on the blockchain → see [What verify() returns](#what-verify-returns)
200
+ - `getProof(hash)` — raw proof, `{ status: "pending" }`, or `null`
201
+
202
+ ### `sha256Hex(textOrBuffer)`
203
+
204
+ SHA-256 of a string (UTF-8) or a Buffer, as a hex string.
205
+
206
+ ### `new BTCNotar(options)` — your own notary
207
+
208
+ | option | default | description |
209
+ |---|---|---|
210
+ | `wif` | — (required) | private key that pays and signs |
211
+ | `network` | `"mainnet"` | `"mainnet"` or `"testnet"` |
212
+ | `store` | `MemoryStore` (lost on exit) | use `FileStore({ filePath })` to keep data |
213
+ | `intervalMs` | 24 hours | `start()` anchors when the oldest queued hash is this old |
214
+ | `feeRateSatVb` | live from mempool.space | fixed fee (sat/vB) |
215
+ | `provider` | `MempoolProvider` | blockchain backend |
216
+
217
+ Methods: `address`, `addHash(hash)`, `pendingCount()`, `settle()`, `anchorNow(hash)`, `start()`, `stop()`, `settleIfDue()`, `getStatus(hash)`, `getProof(hash)`, `verify(hash)`, `refreshStatus(batchId)`.
218
+
219
+ ### Other exports
220
+
221
+ `MerkleTree`, `verifyProof(hash, proof, provider)`, `MemoryStore`, `FileStore`, `MempoolProvider`, `DEFAULT_SERVER_URL`.
222
+
223
+ ---
224
+
225
+ ## How it looks on the blockchain
226
+
227
+ Every batch is one transaction with an `OP_RETURN` output:
164
228
 
165
229
  ```
166
- "BTN1" (4 bytes, protocol magic) || Merkle root (32 bytes)
230
+ 42544e31 + <Merkle root, 32 bytes>
231
+ "BTN1"
167
232
  ```
168
233
 
169
- 36 bytes total, well within Bitcoin's 80-byte `OP_RETURN` limit. The magic
170
- prefix lets `verify()` recognize a BTCNotar anchor (and reject unrelated
171
- `OP_RETURN` data) before trusting its contents as a Merkle root.
234
+ Your hash is not visible on the blockchain — only the Merkle root of all hashes in the batch.
235
+ The proof connects your hash to that root.
236
+
237
+ Merkle tree: leaf = `sha256(0x00 || hash)`, node = `sha256(0x01 || left || right)`, an odd node is moved up unchanged.
172
238
 
173
239
  ## License
174
240
 
package/bin/cli.js CHANGED
@@ -1,12 +1,16 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import "dotenv/config";
3
- import { BTCNotar } from "../index.js";
3
+ import { BTCNotar, BTCNotarClient, sha256Hex } from "../index.js";
4
4
  import { FileStore } from "../src/store.js";
5
5
 
6
6
  function usage() {
7
7
  console.log(`btcnotar - batch Bitcoin notarization CLI
8
8
 
9
- Usage:
9
+ Free public server (no key, no bitcoin needed):
10
+ btcnotar submit <hash|text> Send a hash to the server queue (text is hashed with SHA-256)
11
+ btcnotar check <hash|text> Get the proof from the server and verify it on the blockchain
12
+
13
+ Own notary (needs BTCNOTAR_WIF and a funded address):
10
14
  btcnotar add <hash> Queue a document hash for the next settlement
11
15
  btcnotar settle Settle all pending hashes now (build Merkle tree + anchor on-chain)
12
16
  btcnotar proof <hash> Print the stored proof for a hash
@@ -14,6 +18,7 @@ Usage:
14
18
  btcnotar address Print the notary's Bitcoin address
15
19
 
16
20
  Environment variables:
21
+ BTCNOTAR_SERVER Server for submit/check (or --server <url>). Default: the free public server
17
22
  BTCNOTAR_WIF Bitcoin private key (WIF) used to fund/sign anchor transactions [required]
18
23
  BTCNOTAR_NETWORK "mainnet" or "testnet" (default: mainnet)
19
24
  BTCNOTAR_STORE Path to the JSON store file (default: ./.btcnotar/store.json)
@@ -31,11 +36,37 @@ function buildNotary() {
31
36
  return new BTCNotar({ wif, network, store: new FileStore({ filePath }) });
32
37
  }
33
38
 
39
+ function toHash(input) {
40
+ if (!input) throw new Error("give a hash or a text");
41
+ return /^[0-9a-fA-F]{64}$/.test(input) ? input.toLowerCase() : sha256Hex(input);
42
+ }
43
+
44
+ function buildClient(args) {
45
+ const i = args.indexOf("--server");
46
+ const serverUrl = (i >= 0 ? args[i + 1] : process.env.BTCNOTAR_SERVER) || undefined; // undefined = public server
47
+ if (i >= 0) args.splice(i, 2);
48
+ return new BTCNotarClient({ serverUrl, network: process.env.BTCNOTAR_NETWORK || "mainnet" });
49
+ }
50
+
34
51
  async function main() {
35
- const [cmd, arg] = process.argv.slice(2);
52
+ const args = process.argv.slice(2);
53
+ const cmd = args[0];
36
54
 
37
55
  if (!cmd || cmd === "-h" || cmd === "--help") return usage();
38
56
 
57
+ if (cmd === "submit" || cmd === "check") {
58
+ const client = buildClient(args);
59
+ const input = args.slice(1).join(" ");
60
+ const hash = toHash(input);
61
+ if (hash !== input.toLowerCase()) console.log(`SHA-256: ${hash}`);
62
+ const out = cmd === "submit" ? await client.submit(hash) : await client.verify(hash);
63
+ console.log(JSON.stringify(out, null, 2));
64
+ if (cmd === "check") process.exit(out.valid ? 0 : 1);
65
+ return;
66
+ }
67
+
68
+ const arg = args[1];
69
+
39
70
  if (cmd === "address") {
40
71
  const notary = buildNotary();
41
72
  console.log(notary.address);
package/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { BTCNotar } from "./src/notary.js";
2
+ export { BTCNotarClient, sha256Hex, DEFAULT_SERVER_URL } from "./src/client.js";
3
+ export { verifyProof } from "./src/verify.js";
2
4
  export { MerkleTree } from "./src/merkle.js";
3
5
  export { MemoryStore, FileStore } from "./src/store.js";
4
6
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "btcnotar",
3
- "version": "0.1.0",
4
- "description": "Batch Bitcoin notarization: aggregate document hashes into a Merkle tree, anchor the root on-chain via OP_RETURN on a schedule, and generate per-hash proofs that can be verified independently against the live blockchain (transaction, block and date).",
3
+ "version": "0.2.1",
4
+ "description": "Free Bitcoin notarization: prove a text or file existed at a certain time. Send a SHA-256 hash, get a proof you can verify yourself on the Bitcoin blockchain (Merkle tree + OP_RETURN).",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -30,7 +30,9 @@
30
30
  "timestamping",
31
31
  "op_return",
32
32
  "proof-of-existence",
33
- "blockchain"
33
+ "blockchain",
34
+ "timestamp",
35
+ "sha256"
34
36
  ],
35
37
  "author": "Yordan Karparov",
36
38
  "license": "MIT",
@@ -45,5 +47,6 @@
45
47
  "dotenv": "^17.2.3",
46
48
  "ecpair": "^3.0.0",
47
49
  "tiny-secp256k1": "^2.2.4"
48
- }
50
+ },
51
+ "homepage": "https://github.com/yordank/BTCNotar#readme"
49
52
  }
package/src/client.js ADDED
@@ -0,0 +1,68 @@
1
+ import crypto from "node:crypto";
2
+ import axios from "axios";
3
+ import { MempoolProvider } from "./bitcoin.js";
4
+ import { verifyProof } from "./verify.js";
5
+
6
+ /** SHA-256 of a string (UTF-8) or Buffer, as hex. */
7
+ export function sha256Hex(data) {
8
+ return crypto.createHash("sha256").update(data).digest("hex");
9
+ }
10
+
11
+ /** The free public BTCNotar server (used when no serverUrl is given). */
12
+ export const DEFAULT_SERVER_URL = "https://proofofexistance-production.up.railway.app";
13
+
14
+ function checkHash(hashHex) {
15
+ const hash = String(hashHex || "").trim().toLowerCase();
16
+ if (!/^[0-9a-f]{64}$/.test(hash)) throw new Error(`hash must be 64 hex chars (SHA-256), got "${hashHex}"`);
17
+ return hash;
18
+ }
19
+
20
+ /**
21
+ * Client for a public BTCNotar server. No private key and no bitcoin needed:
22
+ * the server anchors queued hashes on-chain and pays the fee.
23
+ *
24
+ * const client = new BTCNotarClient(); // free public server by default
25
+ * await client.submit(sha256Hex("my document")); // queued
26
+ * ...after the next settlement...
27
+ * await client.verify(hash); // { valid, confirmed, txid, blockHeight, date }
28
+ *
29
+ * verify() does not trust the server: it takes the proof from the server and
30
+ * checks it against the blockchain itself (via mempool.space by default).
31
+ */
32
+ export class BTCNotarClient {
33
+ constructor({ serverUrl = DEFAULT_SERVER_URL, network = "mainnet", provider, timeoutMs = 20_000 } = {}) {
34
+ this.serverUrl = serverUrl.replace(/\/+$/, "");
35
+ this.provider = provider || new MempoolProvider({ network });
36
+ this.http = axios.create({ baseURL: this.serverUrl, timeout: timeoutMs, validateStatus: () => true });
37
+ }
38
+
39
+ /** Send a hash to the server's queue. Returns { hash, addedAt, nextSettleAt, ... }. */
40
+ async submit(hashHex) {
41
+ const hash = checkHash(hashHex);
42
+ const res = await this.http.post("/api/notary/hash", { hashHex: hash });
43
+ if (res.status >= 400) throw new Error(`server error ${res.status}: ${res.data?.error || res.statusText}`);
44
+ return res.data;
45
+ }
46
+
47
+ /**
48
+ * Ask the server for the proof.
49
+ * Returns the proof record, { status: "pending", ... } while waiting, or null if the server doesn't know the hash.
50
+ */
51
+ async getProof(hashHex) {
52
+ const hash = checkHash(hashHex);
53
+ const res = await this.http.get(`/api/notary/proof/${hash}`);
54
+ if (res.status === 404) return null;
55
+ if (res.status === 202) return { status: "pending", ...res.data };
56
+ if (res.status >= 400) throw new Error(`server error ${res.status}: ${res.data?.error || res.statusText}`);
57
+ return res.data;
58
+ }
59
+
60
+ /** Get the proof from the server and verify it against the blockchain yourself. */
61
+ async verify(hashHex) {
62
+ const hash = checkHash(hashHex);
63
+ const record = await this.getProof(hash);
64
+ if (!record) return { valid: false, reason: "unknown-hash" };
65
+ if (record.status === "pending") return { valid: false, reason: "pending", nextSettleAt: record.nextSettleAt ?? null };
66
+ return verifyProof(hash, record, this.provider);
67
+ }
68
+ }
package/src/notary.js CHANGED
@@ -3,7 +3,8 @@ import * as bitcoin from "bitcoinjs-lib";
3
3
  import * as ecc from "tiny-secp256k1";
4
4
  import { ECPairFactory } from "ecpair";
5
5
  import { MerkleTree } from "./merkle.js";
6
- import { MempoolProvider, createOpReturnTx, encodeOpReturnPayload, extractOpReturnData } from "./bitcoin.js";
6
+ import { MempoolProvider, createOpReturnTx, encodeOpReturnPayload } from "./bitcoin.js";
7
+ import { verifyProof } from "./verify.js";
7
8
  import { MemoryStore } from "./store.js";
8
9
 
9
10
  bitcoin.initEccLib(ecc);
@@ -195,6 +196,26 @@ export class BTCNotar {
195
196
  };
196
197
  }
197
198
 
199
+ /** When the queue will be anchored next (oldest pending addedAt + intervalMs), or null if empty. */
200
+ async nextSettleAt() {
201
+ const pending = await this.store.listPending();
202
+ if (!pending.length) return null;
203
+ const oldest = Math.min(...pending.map((p) => Date.parse(p.addedAt) || Date.now()));
204
+ return new Date(oldest + this.intervalMs).toISOString();
205
+ }
206
+
207
+ /**
208
+ * Where is this hash? { status: "anchored", ...proof } | { status: "pending", addedAt, nextSettleAt } | null
209
+ */
210
+ async getStatus(hashHex) {
211
+ const hash = String(hashHex).toLowerCase();
212
+ const proof = await this.getProof(hash);
213
+ if (proof) return { status: "anchored", ...proof };
214
+ const queued = (await this.store.listPending()).find((p) => p.hash === hash);
215
+ if (queued) return { status: "pending", hash, addedAt: queued.addedAt, nextSettleAt: await this.nextSettleAt() };
216
+ return null;
217
+ }
218
+
198
219
  /** Re-fetch a batch's anchoring transaction and update its confirmation status. */
199
220
  async refreshStatus(batchId) {
200
221
  const batch = await this.store.getBatch(batchId);
@@ -221,31 +242,7 @@ export class BTCNotar {
221
242
  * the transaction id, block hash/height and date.
222
243
  */
223
244
  async verify(hashHex, proofRecord) {
224
- const hash = hashHex.toLowerCase();
225
- const record = proofRecord || (await this.getProof(hash));
226
- if (!record) return { valid: false, reason: "no-proof-found" };
227
-
228
- if (!MerkleTree.verifyProof(hash, record.proof, record.merkleRoot)) {
229
- return { valid: false, reason: "merkle-proof-mismatch" };
230
- }
231
-
232
- const tx = await this.provider.fetchTransaction(record.txid);
233
- if (!tx) return { valid: false, reason: "transaction-not-found", txid: record.txid };
234
-
235
- const onChainRoot = extractOpReturnData(tx);
236
- if (!onChainRoot || onChainRoot !== record.merkleRoot) {
237
- return { valid: false, reason: "on-chain-data-mismatch", txid: record.txid };
238
- }
239
-
240
- return {
241
- valid: true,
242
- confirmed: !!tx.confirmed,
243
- txid: record.txid,
244
- merkleRoot: record.merkleRoot,
245
- blockHeight: tx.blockHeight ?? null,
246
- blockHash: tx.blockHash ?? null,
247
- blockTime: tx.blockTime ?? null,
248
- date: tx.blockTime ? new Date(tx.blockTime * 1000).toISOString() : null,
249
- };
245
+ const record = proofRecord || (await this.getProof(hashHex));
246
+ return verifyProof(hashHex, record, this.provider);
250
247
  }
251
248
  }
package/src/verify.js ADDED
@@ -0,0 +1,40 @@
1
+ import { MerkleTree } from "./merkle.js";
2
+ import { extractOpReturnData } from "./bitcoin.js";
3
+
4
+ /**
5
+ * Verify a proof record against the live blockchain, trusting nobody:
6
+ * (1) recompute the Merkle root from the hash + proof,
7
+ * (2) fetch the transaction from the chain and check its OP_RETURN carries that root.
8
+ *
9
+ * record: { merkleRoot, proof, txid } (as returned by getProof / a notary server)
10
+ * provider: anything with fetchTransaction(txid), e.g. MempoolProvider
11
+ */
12
+ export async function verifyProof(hashHex, record, provider) {
13
+ const hash = String(hashHex).toLowerCase();
14
+ if (!record || !record.merkleRoot || !record.txid || !Array.isArray(record.proof)) {
15
+ return { valid: false, reason: "no-proof-found" };
16
+ }
17
+
18
+ if (!MerkleTree.verifyProof(hash, record.proof, record.merkleRoot)) {
19
+ return { valid: false, reason: "merkle-proof-mismatch" };
20
+ }
21
+
22
+ const tx = await provider.fetchTransaction(record.txid);
23
+ if (!tx) return { valid: false, reason: "transaction-not-found", txid: record.txid };
24
+
25
+ const onChainRoot = extractOpReturnData(tx);
26
+ if (!onChainRoot || onChainRoot !== record.merkleRoot) {
27
+ return { valid: false, reason: "on-chain-data-mismatch", txid: record.txid };
28
+ }
29
+
30
+ return {
31
+ valid: true,
32
+ confirmed: !!tx.confirmed,
33
+ txid: record.txid,
34
+ merkleRoot: record.merkleRoot,
35
+ blockHeight: tx.blockHeight ?? null,
36
+ blockHash: tx.blockHash ?? null,
37
+ blockTime: tx.blockTime ?? null,
38
+ date: tx.blockTime ? new Date(tx.blockTime * 1000).toISOString() : null,
39
+ };
40
+ }