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.
- package/README.md +181 -147
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,206 +1,240 @@
|
|
|
1
1
|
# btcnotar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Prove that a text or a file existed at a certain time — using the Bitcoin blockchain.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
// notarize-text.mjs
|
|
23
|
+
import { BTCNotarClient, sha256Hex } from "btcnotar";
|
|
34
24
|
|
|
35
|
-
const
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
### 2. Check it later
|
|
50
36
|
|
|
51
|
-
|
|
52
|
-
//
|
|
37
|
+
```js
|
|
38
|
+
// check-text.mjs
|
|
39
|
+
import { BTCNotarClient, sha256Hex } from "btcnotar";
|
|
53
40
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
console.log("SHA-256:", hash);
|
|
65
|
+
console.log(await client.submit(hash));
|
|
66
|
+
```
|
|
76
67
|
|
|
77
|
-
|
|
68
|
+
Keep the file **exactly as it is**. If one byte changes, the hash changes and the proof will not match.
|
|
78
69
|
|
|
79
|
-
|
|
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();
|
|
77
|
+
const client = new BTCNotarClient();
|
|
78
|
+
const texts = ["first text", "second text", "third text"];
|
|
88
79
|
|
|
89
|
-
const
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
87
|
+
fs.writeFileSync("results.json", JSON.stringify(results, null, 2));
|
|
88
|
+
console.log("Saved to results.json");
|
|
94
89
|
```
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
|
|
91
|
+
```js
|
|
92
|
+
// check-many.mjs
|
|
93
|
+
import fs from "node:fs";
|
|
94
|
+
import { BTCNotarClient } from "btcnotar";
|
|
99
95
|
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
114
|
+
You can give a text or a SHA-256 hash (64 hex characters).
|
|
109
115
|
|
|
110
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
152
|
+
If you want your own server, use `BTCNotar` with your own key (WIF) and a funded address.
|
|
157
153
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
await notary.addHash(sha256Hex("my document")); // add to queue
|
|
166
|
+
notary.start(); // anchors automatically
|
|
173
167
|
|
|
174
|
-
|
|
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=...
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
npx btcnotar
|
|
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
|
-
|
|
189
|
-
external scheduler (system cron, a serverless cron trigger) instead of the
|
|
190
|
-
built-in `start()` interval.
|
|
186
|
+
---
|
|
191
187
|
|
|
192
|
-
##
|
|
188
|
+
## API
|
|
193
189
|
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
230
|
+
42544e31 + <Merkle root, 32 bytes>
|
|
231
|
+
"BTN1"
|
|
199
232
|
```
|
|
200
233
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
}
|