btcnotar 0.2.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.
Files changed (2) hide show
  1. package/README.md +181 -147
  2. package/package.json +7 -4
package/README.md CHANGED
@@ -1,206 +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.
12
-
13
- ## Why batching
14
-
15
- Anchoring one hash per transaction is expensive and doesn't scale. Batching
16
- thousands of hashes into one Merkle root per settlement means:
17
-
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.
23
-
24
- ## Install
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.
25
8
 
26
9
  ```bash
27
10
  npm install btcnotar
28
11
  ```
29
12
 
30
- ## Quick start
13
+ ---
14
+
15
+ ## Easy examples
16
+
17
+ All examples use the free public server. Save them as `.mjs` files and run with `node`.
18
+
19
+ ### 1. Notarize a text
31
20
 
32
21
  ```js
33
- import { BTCNotar, FileStore } from "btcnotar";
22
+ // notarize-text.mjs
23
+ import { BTCNotarClient, sha256Hex } from "btcnotar";
34
24
 
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
- });
25
+ const client = new BTCNotarClient();
26
+ const hash = sha256Hex("Hello, Bitcoin!");
41
27
 
42
- // 1. Queue hashes as documents come in (e.g. sha256 of a file)
43
- await notary.addHash("2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881");
28
+ const result = await client.submit(hash);
29
+ console.log(result);
30
+ // { status: "pending", hash: "...", nextSettleAt: "2026-10-25T15:00:00.000Z" }
31
+ ```
44
32
 
45
- // 2. Start automatic daily settlement (builds the Merkle tree + broadcasts
46
- // one OP_RETURN transaction anchoring the root)
47
- notary.start();
33
+ `pending` means: your hash is in the queue. It goes on the blockchain at `nextSettleAt` (about 1 hour).
48
34
 
49
- // ... later, anywhere, even in a different process that only has the hash ...
35
+ ### 2. Check it later
50
36
 
51
- const proof = await notary.getProof(hash);
52
- // { hash, merkleRoot, proof, txid, batchId, blockHeight, blockHash, blockTime, ... }
37
+ ```js
38
+ // check-text.mjs
39
+ import { BTCNotarClient, sha256Hex } from "btcnotar";
53
40
 
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
- // }
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
+ }
64
52
  ```
65
53
 
66
- `verify()` does two independent checks before it reports `valid: true`:
54
+ ### 3. Notarize a file
55
+
56
+ ```js
57
+ // notarize-file.mjs
58
+ import fs from "node:fs";
59
+ import { BTCNotarClient, sha256Hex } from "btcnotar";
67
60
 
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.
61
+ const client = new BTCNotarClient();
62
+ const hash = sha256Hex(fs.readFileSync("contract.pdf"));
72
63
 
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.
64
+ console.log("SHA-256:", hash);
65
+ console.log(await client.submit(hash));
66
+ ```
76
67
 
77
- ## Free public server (no key, no bitcoin)
68
+ Keep the file **exactly as it is**. If one byte changes, the hash changes and the proof will not match.
78
69
 
79
- Don't want to run your own notary? Send hashes to the free public BTCNotar server
80
- (`https://proofofexistance-production.up.railway.app`, used by default).
81
- The server batches them and pays the fee (one transaction per batch).
82
- You verify the result yourself against the blockchain, so you don't have to trust the server.
70
+ ### 4. Many texts, save results, check later
83
71
 
84
72
  ```js
73
+ // many.mjs
74
+ import fs from "node:fs";
85
75
  import { BTCNotarClient, sha256Hex } from "btcnotar";
86
76
 
87
- const client = new BTCNotarClient(); // or { serverUrl: "https://your-own-server" }
77
+ const client = new BTCNotarClient();
78
+ const texts = ["first text", "second text", "third text"];
88
79
 
89
- const hash = sha256Hex(fs.readFileSync("contract.pdf"));
90
- await client.submit(hash); // { status: "pending", nextSettleAt: "..." }
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
+ }
91
86
 
92
- // after the next settlement (see nextSettleAt):
93
- await client.verify(hash); // { valid: true, confirmed: true, txid, blockHeight, date }
87
+ fs.writeFileSync("results.json", JSON.stringify(results, null, 2));
88
+ console.log("Saved to results.json");
94
89
  ```
95
90
 
96
- - `submit(hash)` — add the hash to the server queue.
97
- - `getProof(hash)` — proof record, `{ status: "pending" }` while waiting, or `null`.
98
- - `verify(hash)` — gets the proof from the server and checks it on-chain (mempool.space by default, or pass your own `provider`).
91
+ ```js
92
+ // check-many.mjs
93
+ import fs from "node:fs";
94
+ import { BTCNotarClient } from "btcnotar";
99
95
 
100
- From the command line:
96
+ const client = new BTCNotarClient();
97
+ const results = JSON.parse(fs.readFileSync("results.json", "utf8"));
101
98
 
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
+ }
102
103
  ```
103
- npx btcnotar submit "any text or a sha256 hash"
104
- npx btcnotar check "any text or a sha256 hash"
105
- # own server: add --server https://your-own-server
104
+
105
+ All texts sent in the same hour go into **one** Bitcoin transaction.
106
+
107
+ ### 5. Command line (no code)
108
+
109
+ ```bash
110
+ npx btcnotar submit "Hello, Bitcoin!"
111
+ npx btcnotar check "Hello, Bitcoin!"
106
112
  ```
107
113
 
108
- ## API
114
+ You can give a text or a SHA-256 hash (64 hex characters).
109
115
 
110
- ### `new BTCNotar(options)`
111
-
112
- | option | required | default | description |
113
- |---------------|----------|---------------------|-------------|
114
- | `wif` | yes | — | Bitcoin private key (WIF) used to fund and sign anchor transactions |
115
- | `network` | no | `"mainnet"` | `"mainnet"` or `"testnet"` |
116
- | `store` | no | `new MemoryStore()` | pending hashes + settled batches storage (see below) |
117
- | `provider` | no | `new MempoolProvider()` | chain data source (UTXOs, fees, broadcast, tx lookup) |
118
- | `feeRateSatVb`| no | fetched live | fixed fee rate override (sat/vB) |
119
- | `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) |
120
-
121
- ### Instance methods
122
-
123
- - `addHash(hashHex)` — queue a hex-encoded hash for the next settlement.
124
- - `pendingCount()` — number of hashes waiting to be settled.
125
- - `settle()` — build a Merkle tree over everything pending and anchor its
126
- root in one `OP_RETURN` transaction right now. Returns the batch record
127
- (`{ batchId, merkleRoot, txid, hashes, proofs, ... }`), or `null` if
128
- nothing was pending.
129
- - `start({ intervalMs })` / `stop()` — automatic settlement: checks every minute and anchors when the oldest pending hash is `intervalMs` old. Survives restarts (uses the stored `addedAt`).
130
- - `settleIfDue()` — the check `start()` runs; anchors only if something has waited `intervalMs`.
131
- - `getStatus(hash)` — `{ status: "anchored", ...proof }`, `{ status: "pending", addedAt, nextSettleAt }` or `null`.
132
- - `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.
133
- - `getProof(hashHex)` — look up the stored proof + anchor info for a hash.
134
- - `verify(hashHex)` — cryptographically and on-chain verification (see above).
135
- - `refreshStatus(batchId)` — re-fetch a batch's transaction and update its
136
- confirmation / block info.
137
-
138
- ### `MerkleTree`
139
-
140
- The underlying Merkle tree, usable standalone:
116
+ ---
141
117
 
142
- ```js
143
- import { MerkleTree } from "btcnotar";
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.
144
128
 
145
- const tree = new MerkleTree([hashA, hashB, hashC]);
146
- tree.root; // hex string
147
- const proof = tree.getProof(0); // inclusion proof for hashA
148
- 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
149
137
  ```
150
138
 
151
- Leaves are domain-separated from internal nodes (distinct hash prefixes),
152
- and an unpaired node at the end of a level is carried up unchanged rather
153
- than duplicated — this avoids both the classic leaf/node confusion forgery
154
- 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)
155
151
 
156
- ### Storage
152
+ If you want your own server, use `BTCNotar` with your own key (WIF) and a funded address.
157
153
 
158
- `MemoryStore` (default, non-persistent) and `FileStore` (JSON file on disk)
159
- are included and share the same interface: `addPending`, `listPending`,
160
- `clearPending`, `saveBatch`, `getBatch`, `findBatchByHash`, `listBatches`.
161
- Implement the same interface against a real database for multi-process or
162
- production use.
154
+ ```js
155
+ import { BTCNotar, FileStore, sha256Hex } from "btcnotar";
163
156
 
164
- ### 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
+ });
165
162
 
166
- `MempoolProvider` talks to the public [mempool.space](https://mempool.space)
167
- REST API for UTXOs, fee estimation, broadcasting and transaction lookup.
168
- Supply your own object implementing `fetchUtxos`, `fetchFeeRateSatVb`,
169
- `broadcastTx`, `fetchTransaction` to use a different backend (e.g. a local
170
- `bitcoind` or Electrum server).
163
+ console.log("Fund this address:", notary.address);
171
164
 
172
- ## CLI
165
+ await notary.addHash(sha256Hex("my document")); // add to queue
166
+ notary.start(); // anchors automatically
173
167
 
174
- 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:
175
177
 
176
178
  ```bash
177
- export BTCNOTAR_WIF=... # required
178
- export BTCNOTAR_NETWORK=mainnet
179
- export BTCNOTAR_STORE=./.btcnotar/store.json
180
-
181
- npx btcnotar add <hash>
182
- npx btcnotar settle
183
- npx btcnotar proof <hash>
184
- npx btcnotar verify <hash>
185
- 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
186
184
  ```
187
185
 
188
- `btcnotar settle` is handy if you'd rather trigger settlement from an
189
- external scheduler (system cron, a serverless cron trigger) instead of the
190
- built-in `start()` interval.
186
+ ---
191
187
 
192
- ## How anchoring works on-chain
188
+ ## API
193
189
 
194
- Each settlement broadcasts one transaction with a zero-value `OP_RETURN`
195
- 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:
196
228
 
197
229
  ```
198
- "BTN1" (4 bytes, protocol magic) || Merkle root (32 bytes)
230
+ 42544e31 + <Merkle root, 32 bytes>
231
+ "BTN1"
199
232
  ```
200
233
 
201
- 36 bytes total, well within Bitcoin's 80-byte `OP_RETURN` limit. The magic
202
- prefix lets `verify()` recognize a BTCNotar anchor (and reject unrelated
203
- `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.
204
238
 
205
239
  ## License
206
240
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "btcnotar",
3
- "version": "0.2.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
  }