@shakesco/silent 1.0.0

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 ADDED
@@ -0,0 +1,200 @@
1
+ # @shakesco/silent
2
+
3
+ ## Install
4
+
5
+ To get started, install the package with your package manager.
6
+
7
+ ```shell {filename=cmd}
8
+ npm i @shakesco/silent
9
+ ```
10
+
11
+ After installing:
12
+
13
+ ```js {filename="index.js"}
14
+ const shakesco = require("@shakesco/silent");
15
+ const {
16
+ KeyGeneration,
17
+ SilentPaymentDestination,
18
+ SilentPaymentBuilder,
19
+ ECPrivateInfo,
20
+ Network,
21
+ BitcoinScriptOutput,
22
+ bip32,
23
+ bip39
24
+ } = shakesco;
25
+ ```
26
+
27
+ ### Generate Silent Payment address
28
+
29
+ This will generate the silent payment address. It prepares a receiver to receive silent payments.
30
+ You can generate a silent payment address in three ways:
31
+
32
+ ##### Private Keys
33
+
34
+ If you are not a wallet provider, use this method. More specifically, you can make the user sign a message and then derive `b_scan` and `b_spend` from the resulting [signature](https://cryptobook.nakov.com/digital-signatures/ecdsa-sign-verify-messages#ecdsa-sign) (Use `r` as `b_scan` and `s` as `b_spend` or vice versa).
35
+
36
+ >⚠️ If you are not using this method, ensure that a cryptographically secure random number generator is being used.
37
+
38
+ ```js {filename="index.js"}
39
+ function main() {
40
+ const b_scan = "";
41
+ const b_spend = "";
42
+ const keys = KeyGeneration.fromPrivateKeys({
43
+ b_scan: b_scan,
44
+ b_spend: b_spend,
45
+ network: "testnet",
46
+ });
47
+ const silentPaymentAddress = keys.toAddress();
48
+ console.log(silentPaymentAddress); // Silent payment address
49
+ }
50
+ ```
51
+
52
+ ##### Mnemonic and HD Key
53
+
54
+ If you are a wallet provider, use this method.
55
+
56
+ ```js {filename="index.js"}
57
+ function main() {
58
+ const mnemonic = ""; // 12, 15, 24 word phrase
59
+ const keys = KeyGeneration.fromMnemonic(mnemonic);
60
+ const silentPaymentAddress = keys.toAddress();
61
+ console.log(silentPaymentAddress);
62
+
63
+ // const seed = bip39.mnemonicToSeedSync(mnemonic);
64
+ // const node = bip32.fromSeed(seed);
65
+ // const keys = KeyGeneration.fromHd(node);
66
+ // const silentPaymentAddress = keys.toAddress();
67
+ // console.log(silentPaymentAddress);
68
+ }
69
+ ```
70
+
71
+ ### Create a taproot address destination
72
+
73
+ Here is where you create a destination address for the user to send to a newly generated Taproot address, derived from the receiver's silent payment address generated above.
74
+ You will need:
75
+
76
+ 1. The Unspent Transaction Output(UTXO) of the user, hash and output_index.
77
+ 2. The private key of the UTXO in 1 above.
78
+ 3. Amount the user wants to send. Should be in satoshis(1 BTC = 100<sup>6</sup> satoshis)
79
+ 4. Finally, the public keys of the 2 secret shares, `B_scan` and `B_spend`
80
+
81
+ ```js {filename="index.js"}
82
+ function main() {
83
+ const addressPubKeys = KeyGeneration.fromAddress(silentPaymentAddress);
84
+ const vinOutpoints = [
85
+ {
86
+ txid: "367e24cac43a7d77621ceb1cbc1cf4a7719fc81b05b07b38f99b043f4e8b95dc",
87
+ index: 1,
88
+ },
89
+ ];
90
+
91
+ const pubkeys = [
92
+ "025c471f0e7d30d6f9095058bbaedaf13e1de67dbfcbe8328e6378d2a3bfb5cfd0",
93
+ ];
94
+ const UTXOPrivatekey = "";
95
+ const builder = new SilentPaymentBuilder({
96
+ vinOutpoints: vinOutpoints,
97
+ pubkeys: pubkeys,
98
+ }).createOutputs(
99
+ [
100
+ new ECPrivateInfo(
101
+ UTXOPrivatekey,
102
+ false // If the output is from a taproot address
103
+ ),
104
+ ],
105
+ [
106
+ new SilentPaymentDestination({
107
+ amount: 1000,
108
+ network: Network.Testnet,
109
+ version: 0,
110
+ scanPubkey: addressPubKeys.B_scan,
111
+ spendPubkey: addressPubKeys.B_spend,
112
+ }),
113
+ ]
114
+ );
115
+ console.log(builder[silentPaymentAddress][0]); // Access the taproot address and send 1000 satoshis
116
+ }
117
+ ```
118
+
119
+ ### Scan for funds
120
+
121
+ Scanning for funds is a drawback of silent payments. So below is how you can check if a certain transaction belongs to a user. You will need:
122
+
123
+ 1. The transaction input's tx_hash and output_index.
124
+ 2. Public key outputted.
125
+ 3. Script and amount from the outputted taproot address
126
+
127
+ For more info, go [here](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#scanning-silent-payment-eligible-transactions)
128
+
129
+ ```js {filename="index.js"}
130
+ function main() {
131
+ const vinOutpoints = [
132
+ {
133
+ txid: "367e24cac43a7d77621ceb1cbc1cf4a7719fc81b05b07b38f99b043f4e8b95dc",
134
+ index: 1,
135
+ },
136
+ ];
137
+
138
+ const pubkeys = [
139
+ "025c471f0e7d30d6f9095058bbaedaf13e1de67dbfcbe8328e6378d2a3bfb5cfd0",
140
+ ];
141
+ const search = new SilentPaymentBuilder({
142
+ vinOutpoints: vinOutpoints,
143
+ pubkeys: pubkeys,
144
+ network: Network.Testnet,
145
+ }).scanOutputs(keys.b_scan, keys.B_spend, [
146
+ new BitcoinScriptOutput(
147
+ "5120fdcb28bcea339a5d36d0c00a3e110b837bf1151be9e7ac9a8544e18b2f63307d",
148
+ BigInt(1000)
149
+ ),
150
+ ]);
151
+
152
+ console.log(
153
+ search[builder[keys.toAddress()][0].address.pubkey.toString("hex")].output
154
+ );
155
+ }
156
+ ```
157
+
158
+ If the address above matches the taproot address from the output in the transaction, it belongs to the user.
159
+
160
+ ### Spend funds
161
+
162
+ If the funds belong to the user, they can spend like so:
163
+
164
+ First, you will need:
165
+
166
+ 1. The transaction input's tx_hash and output_index.
167
+ 2. Public key outputted.
168
+ 3. Receiver's spend and scan private keys.
169
+
170
+ ```js {filename="index.js"}
171
+ function main() {
172
+ const vinOutpoints = [
173
+ {
174
+ txid: "367e24cac43a7d77621ceb1cbc1cf4a7719fc81b05b07b38f99b043f4e8b95dc",
175
+ index: 1,
176
+ },
177
+ ];
178
+
179
+ const pubkeys = [
180
+ "025c471f0e7d30d6f9095058bbaedaf13e1de67dbfcbe8328e6378d2a3bfb5cfd0",
181
+ ];
182
+ const private_key = new SilentPaymentBuilder({
183
+ vinOutpoints: vinOutpoints,
184
+ pubkeys: pubkeys,
185
+ }).spendOutputs(keys.b_scan, keys.b_spend);
186
+
187
+ console.log(private_key); // use this to build a taproot transaction with bitcoinjs: https://github.com/bitcoinjs/bitcoinjs-lib
188
+ }
189
+ ```
190
+
191
+ The receiver can use `private_key` to spend the funds!
192
+
193
+ Thats it! 🎊🎊🎊
194
+
195
+ ### Contribute
196
+
197
+ If you love what we do to progress privacy, [contribute](https://me-qr.com/text/vPod5qN0 "btc_addr") to further development
198
+
199
+ <img src="./images/bitcoin.png" alt="btc_addr" style="display: inline-block; margin-right: 100px; margin-left: 70px;" width="200">
200
+ <img src="./images/silent.png" alt="silent_addr" width="200" style="display: inline-block; margin-right: 10px;">
@@ -0,0 +1,337 @@
1
+ const elliptic = require("elliptic");
2
+ const ec = new elliptic.ec("secp256k1");
3
+ const BN = require("bn.js");
4
+ const { toTaprootAddress } = require("../utils/taproot");
5
+ const {
6
+ toBytes,
7
+ taggedHash,
8
+ toTweakedTaprootKey,
9
+ negate,
10
+ tweakAddPrivate,
11
+ tweakMulPrivate,
12
+ tweakMulPublic,
13
+ tweakAddPublic,
14
+ pubNegate,
15
+ } = require("../utils/utils");
16
+ const SilentPaymentScanningOutput = require("../utils/output");
17
+
18
+ /**
19
+ * This class helps you create a destination taproot address, scan and spend
20
+ * silent payment.
21
+ */
22
+
23
+ class SilentPaymentBuilder {
24
+ constructor({ vinOutpoints, pubkeys, network = "mainnet", receiverTweak }) {
25
+ this.vinOutpoints = vinOutpoints;
26
+ this.pubkeys = pubkeys;
27
+ this.receiverTweak = receiverTweak;
28
+ this.network = network;
29
+ this.A_sum = null;
30
+ this.inputHash = null;
31
+
32
+ if (receiverTweak == null && pubkeys != null) {
33
+ this._getAsum();
34
+ this._getInputHash();
35
+ }
36
+ }
37
+
38
+ _getAsum() {
39
+ const head = this.pubkeys[0];
40
+ const tail = this.pubkeys.slice(1);
41
+
42
+ this.A_sum = tail.reduce((acc, item) => {
43
+ const accPoint = ec.keyFromPublic(acc, "hex").getPublic();
44
+ const itemPoint = ec.keyFromPublic(item, "hex").getPublic();
45
+ return accPoint.add(itemPoint).encode("hex", true);
46
+ }, head);
47
+ }
48
+
49
+ _getInputHash() {
50
+ const sortedOutpoints = this.vinOutpoints.map((outpoint) => {
51
+ const txidBuffer = Buffer.from(outpoint.txid, "hex").reverse();
52
+ const indexBuffer = Buffer.alloc(4);
53
+ indexBuffer.writeUInt32LE(outpoint.index);
54
+ return Buffer.concat([txidBuffer, indexBuffer]);
55
+ });
56
+
57
+ sortedOutpoints.sort(Buffer.compare);
58
+ const lowestOutpoint = sortedOutpoints[0];
59
+
60
+ const A_sumBuffer = Buffer.from(this.A_sum, "hex");
61
+ this.inputHash = taggedHash(
62
+ Buffer.concat([lowestOutpoint, A_sumBuffer]),
63
+ "BIP0352/Inputs"
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Create a destination taproot address for each silent payment address
69
+ * @param inputPrivKeyInfos Private key for each transaction output. Use ECPrivateInfo
70
+ * @param silentPaymentDestinations Destination of the silent payment. Use SilentPaymentDestination
71
+ * @returns Object pointing each silent payment address to the destination taproot address
72
+ */
73
+
74
+ createOutputs(inputPrivKeyInfos, silentPaymentDestinations) {
75
+ let a_sum = null;
76
+ let network;
77
+
78
+ for (const info of inputPrivKeyInfos) {
79
+ let k = ec.keyFromPrivate(info.privkey);
80
+ const isTaproot = info.isTaproot;
81
+
82
+ if (isTaproot) {
83
+ if (info.tweak) {
84
+ k = toTweakedTaprootKey(k);
85
+ }
86
+
87
+ const xOnlyPubkey = k.getPublic();
88
+ const isOdd = xOnlyPubkey.getY().isOdd();
89
+
90
+ if (isOdd) {
91
+ k = negate(k);
92
+ }
93
+ }
94
+
95
+ if (a_sum === null) {
96
+ a_sum = k;
97
+ } else {
98
+ a_sum = tweakAddPrivate(a_sum, k.getPrivate());
99
+ }
100
+ }
101
+
102
+ this.A_sum = a_sum.getPublic().encode("hex", true);
103
+ this._getInputHash();
104
+
105
+ const silentPaymentGroups = {};
106
+
107
+ for (const silentPaymentDestination of silentPaymentDestinations) {
108
+ const B_scan = silentPaymentDestination.B_scan;
109
+ network = silentPaymentDestination.network;
110
+ const scanPubkey = B_scan;
111
+
112
+ if (silentPaymentGroups[scanPubkey]) {
113
+ const group = silentPaymentGroups[scanPubkey];
114
+ const ecdhSharedSecret = Object.keys(group)[0];
115
+ const recipients = group[ecdhSharedSecret];
116
+
117
+ silentPaymentGroups[scanPubkey] = {
118
+ [ecdhSharedSecret]: [...recipients, silentPaymentDestination],
119
+ };
120
+ } else {
121
+ const senderPartialSecret = tweakMulPrivate(
122
+ a_sum,
123
+ new BN(this.inputHash)
124
+ );
125
+ const ecdhSharedSecret = tweakMulPublic(
126
+ ec.keyFromPublic(B_scan, "hex").getPublic(),
127
+ senderPartialSecret.getPrivate()
128
+ ).encode("hex", true);
129
+
130
+ silentPaymentGroups[scanPubkey] = {
131
+ [ecdhSharedSecret]: [silentPaymentDestination],
132
+ };
133
+ }
134
+ }
135
+
136
+ const result = {};
137
+
138
+ for (const [scanPubkey, group] of Object.entries(silentPaymentGroups)) {
139
+ const ecdhSharedSecret = Object.keys(group)[0];
140
+ const destinations = group[ecdhSharedSecret];
141
+
142
+ let k = 0;
143
+ for (const destination of destinations) {
144
+ const t_k = taggedHash(
145
+ Buffer.concat([
146
+ Buffer.from(
147
+ ec
148
+ .keyFromPublic(Buffer.from(ecdhSharedSecret, "hex"))
149
+ .getPublic()
150
+ .encodeCompressed(),
151
+ "array"
152
+ ),
153
+ Buffer.from(toBytes(BigInt(k), 4), "array"),
154
+ ]),
155
+ "BIP0352/SharedSecret"
156
+ );
157
+
158
+ const P_mn = tweakAddPublic(
159
+ ec.keyFromPublic(destination.B_spend, "hex").getPublic(),
160
+ new BN(t_k)
161
+ );
162
+
163
+ const resOutput = {
164
+ address: toTaprootAddress(P_mn, network, { tweak: false }),
165
+ amount: destination.amount,
166
+ };
167
+
168
+ if (result[destination.toString()]) {
169
+ result[destination.toString()].push(resOutput);
170
+ } else {
171
+ result[destination.toString()] = [resOutput];
172
+ }
173
+
174
+ k++;
175
+ }
176
+ }
177
+
178
+ return result;
179
+ }
180
+
181
+ /**
182
+ * Scan every transaction on the network to find users silent payments
183
+ * Check here to see valid checks: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#scanning-silent-payment-eligible-transactions
184
+ * @param b_scan Scan private key.
185
+ * @param B_spend Spend Public key
186
+ * @param outputsToCheck Script and amount to check. Use BitcoinScriptOutput
187
+ * @param precomputedLabels Optional labels to differentiate silent payments if already precomputed.
188
+ * @returns Silent payment address and the amount
189
+ */
190
+
191
+ scanOutputs(b_scan, B_spend, outputsToCheck, precomputedLabels = {}) {
192
+ const tweakDataForRecipient = this.receiverTweak
193
+ ? ec.keyFromPublic(this.receiverTweak).getPublic()
194
+ : tweakMulPublic(
195
+ ec.keyFromPublic(Buffer.from(this.A_sum, "hex")).getPublic(),
196
+ this.inputHash
197
+ );
198
+ const ecdhSharedSecret = tweakMulPublic(tweakDataForRecipient, b_scan);
199
+
200
+ const matches = {};
201
+ var k = 0;
202
+
203
+ do {
204
+ const t_k = taggedHash(
205
+ Buffer.concat([
206
+ Buffer.from(ecdhSharedSecret.encodeCompressed(), "array"),
207
+ Buffer.from(toBytes(BigInt(k), 4), "array"),
208
+ ]),
209
+ "BIP0352/SharedSecret"
210
+ );
211
+
212
+ const P_k = tweakAddPublic(B_spend, t_k);
213
+ const length = outputsToCheck.length;
214
+
215
+ for (var i = 0; i < length; i++) {
216
+ const output = outputsToCheck[i].script.slice(4);
217
+ const outputPubkey = output.toString("hex");
218
+ const outputAmount = Number(outputsToCheck[i].value);
219
+
220
+ if (
221
+ Buffer.compare(
222
+ Buffer.from(output, "hex"),
223
+ Buffer.from(P_k.encodeCompressed().slice(1), "array")
224
+ ) === 0
225
+ ) {
226
+ matches[outputPubkey] = new SilentPaymentScanningOutput({
227
+ output: new SilentPaymentOutput(
228
+ toTaprootAddress(P_k, this.network, {
229
+ tweak: false,
230
+ }),
231
+ outputAmount
232
+ ),
233
+ tweak: t_k.toString("hex"),
234
+ });
235
+
236
+ outputsToCheck.splice(i, 1);
237
+ k++;
238
+ break;
239
+ }
240
+
241
+ if (precomputedLabels != null && precomputedLabels.isNotEmpty) {
242
+ var m_G_sub = tweakAddPublic(
243
+ ec.keyFromPublic(Buffer.from(output, "hex")).getPublic(),
244
+ pubNegate(P_k)
245
+ );
246
+ var m_G =
247
+ precomputedLabels[
248
+ ec.keyFromPublic(m_G_sub).getPublic().encodeCompressed("hex")
249
+ ];
250
+
251
+ if (!m_G) {
252
+ m_G_sub = ec
253
+ .keyFromPublic(Buffer.from(output, "hex"))
254
+ .getPublic()
255
+ .add(pubNegate(P_k));
256
+ m_G =
257
+ precomputedLabels[
258
+ ec.keyFromPublic(m_G_sub).getPublic().encodeCompressed("hex")
259
+ ];
260
+ }
261
+
262
+ if (m_G) {
263
+ const P_km = tweakAddPublic(P_k, m_G);
264
+
265
+ matches[outputPubkey] = new SilentPaymentScanningOutput({
266
+ output: new SilentPaymentOutput(
267
+ toTaprootAddress(P_km, this.network, {
268
+ tweak: false,
269
+ }),
270
+ outputAmount
271
+ ),
272
+ tweak: tweakAddPrivate(ec.keyFromPrivate(t_k).getPrivate(), m_G)
273
+ .getPrivate()
274
+ .toString("hex"),
275
+ label: m_G,
276
+ });
277
+
278
+ outputsToCheck.splice(i, 1);
279
+ k++;
280
+ break;
281
+ }
282
+ }
283
+
284
+ outputsToCheck.splice(i, 1);
285
+
286
+ if (i + 1 >= outputsToCheck.length) {
287
+ break;
288
+ }
289
+ }
290
+ } while (outputsToCheck.isNotEmpty);
291
+
292
+ return matches;
293
+ }
294
+
295
+ /**
296
+ * Spend the silent payment
297
+ * @param b_scan Scan private key
298
+ * @param b_spend Spend private Key
299
+ * @returns
300
+ */
301
+
302
+ spendOutputs(b_scan, b_spend) {
303
+ const tweakDataForRecipient = this.receiverTweak
304
+ ? ec.keyFromPublic(this.receiverTweak).getPublic()
305
+ : tweakMulPublic(
306
+ ec.keyFromPublic(Buffer.from(this.A_sum, "hex")).getPublic(),
307
+ this.inputHash
308
+ );
309
+ const ecdhSharedSecret = tweakMulPublic(tweakDataForRecipient, b_scan);
310
+
311
+ var k = 0;
312
+
313
+ const t_k = taggedHash(
314
+ Buffer.concat([
315
+ Buffer.from(ecdhSharedSecret.encodeCompressed(), "array"),
316
+ Buffer.from(toBytes(BigInt(k), 4), "array"),
317
+ ]),
318
+ "BIP0352/SharedSecret"
319
+ );
320
+
321
+ const p_k = tweakAddPrivate(
322
+ ec.keyFromPrivate(b_spend.toString("hex")),
323
+ new BN(t_k)
324
+ );
325
+
326
+ return p_k.getPrivate().toString("hex");
327
+ }
328
+ }
329
+
330
+ class SilentPaymentOutput {
331
+ constructor(address, value) {
332
+ this.address = address;
333
+ this.value = value;
334
+ }
335
+ }
336
+
337
+ module.exports = SilentPaymentBuilder;
@@ -0,0 +1,195 @@
1
+ const EC = require("elliptic").ec;
2
+ const ec = new EC("secp256k1");
3
+ const tinysecp = require("tiny-secp256k1");
4
+ const { BIP32Factory } = require("bip32");
5
+ const bip39 = require("bip39");
6
+ const {
7
+ encodeBech32,
8
+ convertToBase32,
9
+ convertFromBase32,
10
+ decodeBech32,
11
+ } = require("../utils/bech32");
12
+ const Network = require("../utils/network");
13
+ const bip32 = BIP32Factory(tinysecp);
14
+
15
+ const SCAN_PATH = "m/352'/1'/0'/1'/0";
16
+
17
+ const SPEND_PATH = "m/352'/1'/0'/0'/0";
18
+
19
+ class SilentPaymentAddress {
20
+ static get regex() {
21
+ return /(^|\s)t?sp(rt)?1[0-9a-zA-Z]{113}($|\s)/;
22
+ }
23
+
24
+ constructor({ B_scan, B_spend, network = Network.Mainnet, version = 0 }) {
25
+ this.B_scan = B_scan;
26
+ this.B_spend = B_spend;
27
+ this.network = network;
28
+ this.version = version;
29
+ this.hrp = this.network === Network.Testnet ? "tsp" : "sp";
30
+
31
+ // Version validation
32
+ if (this.version !== 0) {
33
+ throw new Error("Can't have other version than 0 for now");
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Returns silent address public keys
39
+ * @param address The silent payment address
40
+ * @returns Scan public key and Spend public key
41
+ */
42
+
43
+ static fromAddress(address) {
44
+ const decoded = decodeBech32(address);
45
+ const prefix = decoded[0];
46
+ const words = decoded[1];
47
+
48
+ if (prefix !== "sp" && prefix !== "sprt" && prefix !== "tsp") {
49
+ throw new Error(`Invalid prefix: ${prefix}`);
50
+ }
51
+
52
+ const version = words[0];
53
+ if (version !== 0) throw new Error("Invalid version");
54
+
55
+ // Convert words to bytes (base32 to bytes)
56
+ const key = convertFromBase32(words.slice(1));
57
+
58
+ return new SilentPaymentAddress({
59
+ B_scan: ec.keyFromPublic(key.slice(0, 33)).getPublic(),
60
+ B_spend: ec.keyFromPublic(key.slice(33)).getPublic(),
61
+ network: prefix === "tsp" ? Network.Testnet : Network.Mainnet,
62
+ version: version,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Get silent payment address
68
+ * @returns Silent payment address
69
+ */
70
+
71
+ toAddress() {
72
+ return this.toString();
73
+ }
74
+
75
+ static toString() {
76
+ const encodedResult = encodeBech32(this.hrp, [
77
+ this.version,
78
+ ...convertToBase32([
79
+ ...this.B_scan.encodeCompressed("array"),
80
+ ...this.B_spend.encodeCompressed("array"),
81
+ ]),
82
+ ]);
83
+
84
+ return encodedResult;
85
+ }
86
+ }
87
+
88
+ class SilentPaymentDestination extends SilentPaymentAddress {
89
+ constructor({ version, scanPubkey, spendPubkey, network, amount }) {
90
+ super({
91
+ version,
92
+ B_scan: scanPubkey,
93
+ B_spend: spendPubkey,
94
+ network: network,
95
+ });
96
+ this.amount = amount;
97
+ }
98
+
99
+ static fromAddress(address, amount) {
100
+ const receiver = SilentPaymentAddress.fromAddress(address);
101
+
102
+ return new SilentPaymentDestination({
103
+ scanPubkey: receiver.B_scan,
104
+ spendPubkey: receiver.B_spend,
105
+ network: receiver.network,
106
+ version: receiver.version,
107
+ amount: amount,
108
+ });
109
+ }
110
+ }
111
+
112
+ // Creating spending and scanning keys
113
+ class KeyGeneration extends SilentPaymentAddress {
114
+ constructor({ version = 0, B_scan, B_spend, b_scan, b_spend, network }) {
115
+ super({
116
+ B_scan: B_scan,
117
+ B_spend: B_spend,
118
+ network: network,
119
+ version: version,
120
+ });
121
+ this.b_scan = b_scan;
122
+ this.b_spend = b_spend;
123
+ this.B_scan = B_scan;
124
+ this.B_spend = B_spend;
125
+ this.network = network;
126
+ }
127
+
128
+ /**
129
+ * Generate silent payment address through private keys
130
+ * @param b_scan Scan private key
131
+ * @param b_spend Spend private key
132
+ * @returns
133
+ */
134
+
135
+ static fromPrivateKeys({
136
+ b_scan,
137
+ b_spend,
138
+ network = Network.Mainnet,
139
+ version = 0,
140
+ }) {
141
+ b_scan = b_scan.startsWith("0x") ? b_scan.slice(2) : b_scan;
142
+ b_spend = b_spend.startsWith("0x") ? b_spend.slice(2) : b_spend;
143
+
144
+ const B_scan = ec.keyFromPrivate(b_scan).getPublic();
145
+ const B_spend = ec.keyFromPrivate(b_spend).getPublic();
146
+
147
+ return new KeyGeneration({
148
+ b_scan: ec.keyFromPrivate(b_scan).getPrivate(),
149
+ b_spend: ec.keyFromPrivate(b_spend).getPrivate(),
150
+ B_scan: B_scan,
151
+ B_spend: B_spend,
152
+ network: network,
153
+ version: version,
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Generate silent payment address through HD keys
159
+ * @param bip32 HD wallet. We have provided an easy way to access bip32
160
+ * @param hrp 'sp' for mainnet, tsp for testnet
161
+ * @returns
162
+ */
163
+
164
+ static fromHd(bip32, { hrp = "sp", version = 0 } = {}) {
165
+ const scanDerivation = bip32.derivePath(SCAN_PATH);
166
+ const spendDerivation = bip32.derivePath(SPEND_PATH);
167
+ return new KeyGeneration({
168
+ b_scan: ec.keyFromPrivate(scanDerivation.privateKey).getPrivate(),
169
+ b_spend: ec.keyFromPrivate(spendDerivation.privateKey).getPrivate(),
170
+ B_scan: ec.keyFromPrivate(scanDerivation.privateKey).getPublic(),
171
+ B_spend: ec.keyFromPrivate(spendDerivation.privateKey).getPublic(),
172
+ network: hrp == "tsp" ? Network.Testnet : Network.Mainnet,
173
+ version: version,
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Generate silent payment address through mnemonic
179
+ * @param mnemonic Mnemonic phrase.
180
+ * @param hrp 'sp' for mainnet, tsp for testnet
181
+ * @returns
182
+ */
183
+
184
+ static fromMnemonic(mnemonic, { hrp = "sp", version = 0 } = {}) {
185
+ return KeyGeneration.fromHd(
186
+ bip32.fromSeed(bip39.mnemonicToSeedSync(mnemonic)),
187
+ {
188
+ hrp: hrp,
189
+ version: version,
190
+ }
191
+ );
192
+ }
193
+ }
194
+
195
+ module.exports = { KeyGeneration, SilentPaymentDestination };
Binary file
Binary file
package/index.js ADDED
@@ -0,0 +1,23 @@
1
+ const {
2
+ KeyGeneration,
3
+ SilentPaymentDestination,
4
+ } = require("./classes/KeyGeneration");
5
+ const SilentPaymentBuilder = require("./classes/CreateOutput");
6
+ const ECPrivateInfo = require("./utils/info");
7
+ const Network = require("./utils/network");
8
+ const BitcoinScriptOutput = require("./utils/scriptOutput");
9
+ const { BIP32Factory } = require("bip32");
10
+ const tinysecp = require("tiny-secp256k1");
11
+ const bip32 = BIP32Factory(tinysecp);
12
+ const bip39 = require("bip39");
13
+
14
+ module.exports = {
15
+ KeyGeneration,
16
+ SilentPaymentDestination,
17
+ SilentPaymentBuilder,
18
+ ECPrivateInfo,
19
+ Network,
20
+ BitcoinScriptOutput,
21
+ bip32,
22
+ bip39,
23
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@shakesco/silent",
3
+ "version": "1.0.0",
4
+ "description": "Bitcoin Silent Payments",
5
+ "main": "index.js",
6
+ "author": "Shawn Kimtai",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "shakesco",
10
+ "shakespay",
11
+ "private",
12
+ "bitcoin",
13
+ "silent payments",
14
+ "ECDH"
15
+ ],
16
+ "dependencies": {
17
+ "elliptic": "^6.5.7"
18
+ },
19
+ "devDependencies": {
20
+ "bip32": "^5.0.0-rc.0",
21
+ "bip39": "^3.1.0",
22
+ "tiny-secp256k1": "^2.2.3"
23
+ }
24
+ }
@@ -0,0 +1,171 @@
1
+ // Bech32 character set for encoding
2
+ const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
3
+
4
+ // Generator coefficients for checksum calculation
5
+ const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
6
+
7
+ const ENCODING_CONST = {
8
+ bech32: 1,
9
+ bech32m: 0x2bc830a3,
10
+ };
11
+
12
+ const Bech32Consts = {
13
+ /// The separator character used in Bech32 encoded strings.
14
+ separator: "1",
15
+
16
+ /// The length of the checksum part in a Bech32 encoded string.
17
+ checksumStrLen: 6,
18
+ };
19
+
20
+ // Bech32 encoding function
21
+ function encodeBech32(
22
+ hrp,
23
+ data,
24
+ separator = Bech32Consts.separator,
25
+ encoding = "bech32m"
26
+ ) {
27
+ const checksum = createChecksum(hrp, data, encoding);
28
+ const combined = [...data, ...checksum];
29
+ const encodedData = combined.map((value) => CHARSET[value]).join("");
30
+ return `${hrp}${separator}${encodedData}`;
31
+ }
32
+
33
+ // Convert bytes to base32
34
+ function convertToBase32(data) {
35
+ const result = [];
36
+ let accumulator = 0;
37
+ let bits = 0;
38
+ const maxV = 31; // 5-bit chunks for base32
39
+
40
+ for (const value of data) {
41
+ accumulator = (accumulator << 8) | value;
42
+ bits += 8;
43
+
44
+ while (bits >= 5) {
45
+ bits -= 5;
46
+ result.push((accumulator >> bits) & maxV);
47
+ }
48
+ }
49
+
50
+ if (bits > 0) {
51
+ result.push((accumulator << (5 - bits)) & maxV);
52
+ }
53
+
54
+ return result;
55
+ }
56
+
57
+ function convertFromBase32(data) {
58
+ const result = [];
59
+ let accumulator = 0;
60
+ let bits = 0;
61
+ const maxV = 255; // 8-bit chunks for bytes
62
+
63
+ for (const value of data) {
64
+ accumulator = (accumulator << 5) | value;
65
+ bits += 5;
66
+
67
+ while (bits >= 8) {
68
+ bits -= 8;
69
+ result.push((accumulator >> bits) & maxV);
70
+ }
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ // Create checksum
77
+ function createChecksum(hrp, data, encoding = "bech32m") {
78
+ const values = [...expandHrp(hrp), ...data, 0, 0, 0, 0, 0, 0];
79
+ const polymod = polyMod(values) ^ ENCODING_CONST[encoding];
80
+ return Array.from({ length: 6 }, (_, i) => (polymod >> (5 * (5 - i))) & 31);
81
+ }
82
+
83
+ // Expand the human-readable part
84
+ function expandHrp(hrp) {
85
+ const expand = [];
86
+ for (let i = 0; i < hrp.length; i++) {
87
+ expand.push(hrp.charCodeAt(i) >> 5);
88
+ }
89
+ expand.push(0);
90
+ for (let i = 0; i < hrp.length; i++) {
91
+ expand.push(hrp.charCodeAt(i) & 31);
92
+ }
93
+ return expand;
94
+ }
95
+
96
+ // PolyMod function for checksum calculation
97
+ function polyMod(values) {
98
+ let chk = 1;
99
+ for (const value of values) {
100
+ const top = chk >> 25;
101
+ chk = ((chk & 0x1ffffff) << 5) ^ value;
102
+ for (let i = 0; i < 5; i++) {
103
+ if ((top >> i) & 1) {
104
+ chk ^= GENERATOR[i];
105
+ }
106
+ }
107
+ }
108
+ return chk;
109
+ }
110
+
111
+ function decodeBech32(
112
+ bechStr,
113
+ sep = Bech32Consts.separator,
114
+ checksumLen = Bech32Consts.checksumStrLen,
115
+ encoding = "bech32m"
116
+ ) {
117
+ if (_isStringMixed(bechStr)) {
118
+ throw new Error("Invalid bech32 format (string is mixed case)");
119
+ }
120
+
121
+ bechStr = bechStr.toLowerCase();
122
+
123
+ const sepPos = bechStr.lastIndexOf(sep);
124
+ if (sepPos == -1) {
125
+ throw new Error("Invalid bech32 format (no separator found)");
126
+ }
127
+
128
+ const hrp = bechStr.substring(0, sepPos);
129
+ if (
130
+ hrp.length === 0 ||
131
+ hrp
132
+ .split("")
133
+ .some((char) => char.charCodeAt(0) < 33 || char.charCodeAt(0) > 126)
134
+ ) {
135
+ throw new Error(`Invalid bech32 format (HRP not valid: ${hrp})`);
136
+ }
137
+
138
+ const dataPart = bechStr.substring(sepPos + 1);
139
+
140
+ if (
141
+ dataPart.length < checksumLen + 1 ||
142
+ dataPart.split("").some((char) => !CHARSET.includes(char))
143
+ ) {
144
+ throw new Error("Invalid bech32 format (data part not valid)");
145
+ }
146
+
147
+ const intData = dataPart.split("").map((char) => CHARSET.indexOf(char));
148
+
149
+ if (!veriCheckSum(hrp, intData, encoding)) {
150
+ throw new Error("Invalid bech32 checksum");
151
+ }
152
+
153
+ return [hrp, Array.from(intData.slice(0, intData.length - checksumLen))];
154
+ }
155
+
156
+ function veriCheckSum(hrp, data, encoding = "bech32m") {
157
+ const polymod = polyMod([...expandHrp(hrp), ...data]);
158
+
159
+ return polymod == ENCODING_CONST[encoding];
160
+ }
161
+
162
+ function _isStringMixed(str) {
163
+ return str !== str.toLowerCase() && str !== str.toUpperCase();
164
+ }
165
+
166
+ module.exports = {
167
+ convertToBase32,
168
+ convertFromBase32,
169
+ encodeBech32,
170
+ decodeBech32,
171
+ };
package/utils/const.js ADDED
@@ -0,0 +1,2 @@
1
+ const TAPROOT_WITNESS_VERSION = 0x01;
2
+ module.exports = { TAPROOT_WITNESS_VERSION };
package/utils/info.js ADDED
@@ -0,0 +1,9 @@
1
+ class ECPrivateInfo {
2
+ constructor(privkey, isTaproot, tweak = false) {
3
+ this.privkey = privkey;
4
+ this.isTaproot = isTaproot;
5
+ this.tweak = tweak;
6
+ }
7
+ }
8
+
9
+ module.exports = ECPrivateInfo;
@@ -0,0 +1,9 @@
1
+ class Network {
2
+ // Bitcoin Mainnet
3
+ static Mainnet = "mainnet";
4
+
5
+ //Bitcoin Testnet
6
+ static Testnet = "testnet";
7
+ }
8
+
9
+ module.exports = Network;
@@ -0,0 +1,9 @@
1
+ class SilentPaymentScanningOutput {
2
+ constructor(output, tweak, label = null) {
3
+ this.output = output;
4
+ this.tweak = tweak;
5
+ this.label = label;
6
+ }
7
+ }
8
+
9
+ module.exports = SilentPaymentScanningOutput;
@@ -0,0 +1,8 @@
1
+ class BitcoinScriptOutput {
2
+ constructor(script, value) {
3
+ this.script = script;
4
+ this.value = value;
5
+ }
6
+ }
7
+
8
+ module.exports = BitcoinScriptOutput;
@@ -0,0 +1,102 @@
1
+ const elliptic = require("elliptic");
2
+ const ec = new elliptic.ec("secp256k1");
3
+ const BN = require("bn.js");
4
+ const { createHash } = require("crypto");
5
+ const { convertToBase32, encodeBech32 } = require("./bech32");
6
+ const { TAPROOT_WITNESS_VERSION } = require("./const");
7
+ const Network = require("./network");
8
+
9
+ class P2trAddress {
10
+ constructor(address, pubkey) {
11
+ this.address = address;
12
+ this.pubkey = pubkey;
13
+ }
14
+
15
+ static saveTaproot({ address, pubkey }) {
16
+ return new P2trAddress(address, pubkey);
17
+ }
18
+ }
19
+
20
+ function toTaprootAddress(
21
+ publicKey,
22
+ network = Network.Mainnet,
23
+ { scripts = null, tweak = true } = {}
24
+ ) {
25
+ const pubKey = toTapRotHex(publicKey, { script: scripts, tweak });
26
+ const words = convertToBase32(Buffer.from(pubKey, "hex"));
27
+ words.unshift(TAPROOT_WITNESS_VERSION);
28
+
29
+ const hrp = network == Network.Testnet ? "tb" : "bc";
30
+ return P2trAddress.saveTaproot({
31
+ address: encodeBech32(hrp, words),
32
+ pubkey: Buffer.from(pubKey, "hex"),
33
+ });
34
+ }
35
+
36
+ function toTapRotHex(pubKey, { script = null, tweak = true }) {
37
+ let point = ec.keyFromPublic(pubKey, "hex").getPublic();
38
+
39
+ if (tweak) {
40
+ const scriptBytes = script?.map((e) => e.map((e) => Buffer.from(e, "hex")));
41
+ point = P2TRUtils.tweakPublicKey(point, { script: scriptBytes });
42
+ }
43
+
44
+ return point.getX().toString("hex").padStart(64, "0");
45
+ }
46
+
47
+ class P2TRUtils {
48
+ static tweakPublicKey(pubPoint, { script = null }) {
49
+ const h = this.calculateTweak(pubPoint, { script });
50
+ const n = ec.g.mul(new BN(h, 16));
51
+ const outPoint = this.liftX(pubPoint).add(n);
52
+
53
+ return outPoint;
54
+ }
55
+
56
+ static liftX(pubKeyPoint) {
57
+ const p = ec.curve.p; // Prime for the secp256k1 curve
58
+ const x = pubKeyPoint.x;
59
+
60
+ // Check if x is valid
61
+ if (x.cmp(p) >= 0) {
62
+ throw new Error("Unable to compute LiftX point");
63
+ }
64
+
65
+ // Compute y^2 = (x^3 + 7) % p
66
+ const ySq = x.pow(new BN(3)).mod(p).add(new BN(7)).mod(p);
67
+
68
+ // Compute y = ySq ^ ((p + 1) / 4) % p
69
+ const y = ySq.pow(p.add(new BN(1)).div(new BN(4))).mod(p);
70
+
71
+ // Check if y^2 == ySq (i.e., the point is on the curve)
72
+ if (y.pow(new BN(2)).mod(p).cmp(ySq) !== 0) {
73
+ throw new Error("Unable to compute LiftX point");
74
+ }
75
+
76
+ // Ensure y is the correct parity (even or odd)
77
+ const result = y.isEven() ? y : p.sub(y);
78
+
79
+ // Return the new point on the curve
80
+ return ec.curve.point(x, result);
81
+ }
82
+
83
+ static calculateTweak(pubPoint, { script = null }) {
84
+ const x = pubPoint.getX().toString("hex").padStart(64, "0");
85
+ let t = Buffer.from(x, "hex");
86
+
87
+ if (script) {
88
+ const h = createHash("sha256");
89
+ h.update(t);
90
+ for (const leaf of script) {
91
+ h.update(Buffer.concat(leaf));
92
+ }
93
+ t = h.digest();
94
+ }
95
+
96
+ return t.toString("hex");
97
+ }
98
+ }
99
+
100
+ module.exports = {
101
+ toTaprootAddress,
102
+ };
package/utils/utils.js ADDED
@@ -0,0 +1,176 @@
1
+ const elliptic = require("elliptic");
2
+ const ec = new elliptic.ec("secp256k1");
3
+ const BN = require("bn.js");
4
+ const { createHash } = require("crypto");
5
+
6
+ function toBytes(bigInt, length = 4) {
7
+ let hex = bigInt.toString(16);
8
+ if (hex.length % 2) {
9
+ hex = "0" + hex; // Ensure the hex string has even length
10
+ }
11
+ const bytes = [];
12
+ for (let i = 0; i < hex.length; i += 2) {
13
+ bytes.push(parseInt(hex.substr(i, 2), 16));
14
+ }
15
+
16
+ // Ensure the byte array has the required length by padding with leading zeros
17
+ while (bytes.length < length) {
18
+ bytes.unshift(0);
19
+ }
20
+ return bytes.slice(-length); // Ensure it's exactly 'length' bytes
21
+ }
22
+
23
+ function tweakAddPrivate(key, tweak) {
24
+ const privateKey = key.getPrivate().add(tweak).umod(ec.curve.n);
25
+ return ec.keyFromPrivate(privateKey);
26
+ }
27
+
28
+ function tweakMulPrivate(key, tweak) {
29
+ const privateKey = key.getPrivate().mul(tweak).umod(ec.curve.n);
30
+ return ec.keyFromPrivate(privateKey);
31
+ }
32
+
33
+ function tweakAddPublic(key, tweak) {
34
+ return key.add(ec.g.mul(tweak));
35
+ }
36
+
37
+ function tweakMulPublic(key, tweak) {
38
+ return key.mul(tweak);
39
+ }
40
+
41
+ function negate(key) {
42
+ const negatedPrivate = ec.curve.n.sub(key.getPrivate());
43
+ return ec.keyFromPrivate(negatedPrivate);
44
+ }
45
+
46
+ function pubNegate(key) {
47
+ // Get the current point
48
+ const point = key;
49
+
50
+ // Negate the Y-coordinate
51
+ const negatedPoint = point.neg();
52
+
53
+ // Convert the negated point to uncompressed format (04 || x || y)
54
+ const xHex = negatedPoint.getX().toString("hex").padStart(64, "0");
55
+ const yHex = negatedPoint.getY().toString("hex").padStart(64, "0");
56
+ const uncompressedHex = "04" + xHex + yHex;
57
+
58
+ // Create and return a new ECPublic instance
59
+ return ec.keyFromPublic(uncompressedHex, "hex");
60
+ }
61
+
62
+ function toTweakedTaprootKey(key) {
63
+ const pubKey = key.getPublic();
64
+ const t = calculateTweek(pubKey);
65
+ return calculatePrivateTweek(key.getPrivate(), new BN(t));
66
+ }
67
+
68
+ function calculateTweek(pubPoint, script = null) {
69
+ const keyX = pubPoint.getX().toArrayLike(Buffer, "be", 32);
70
+ if (script === null) {
71
+ return this.taggedHash("TapTweak", keyX);
72
+ }
73
+ const merkleRoot = this._getTagHashedMerkleRoot(script);
74
+ return this.taggedHash("TapTweak", Buffer.concat([keyX, merkleRoot]));
75
+ }
76
+
77
+ function taggedHash(tag, dataBytes) {
78
+ if (typeof tag !== "string" && !Buffer.isBuffer(tag)) {
79
+ throw new Error("tag must be string or Buffer");
80
+ }
81
+ const tagHash = typeof tag === "string" ? this.sha256(Buffer.from(tag)) : tag;
82
+ return this.sha256(Buffer.concat([tagHash, tagHash, dataBytes]));
83
+ }
84
+
85
+ function _getTagHashedMerkleRoot(args) {
86
+ if (Buffer.isBuffer(args)) {
87
+ return this._tapleafTaggedHash(args);
88
+ }
89
+
90
+ if (!Array.isArray(args)) throw new Error("args must be Buffer or Array");
91
+ if (args.length === 0) return Buffer.alloc(0);
92
+ if (args.length === 1) {
93
+ return this._getTagHashedMerkleRoot(args[0]);
94
+ } else if (args.length === 2) {
95
+ const left = _getTagHashedMerkleRoot(args[0]);
96
+ const right = _getTagHashedMerkleRoot(args[1]);
97
+ return _tapBranchTaggedHash(left, right);
98
+ }
99
+ throw new Error("List cannot have more than 2 branches.");
100
+ }
101
+
102
+ function _tapleafTaggedHash(script) {
103
+ const scriptBytes = this.prependVarint(script);
104
+ const part = Buffer.concat([Buffer.from([0xc0]), scriptBytes]);
105
+ return taggedHash("TapLeaf", part);
106
+ }
107
+
108
+ function prependVarint(data) {
109
+ const varintBytes = this.encodeVarint(data.length);
110
+ return Buffer.concat([varintBytes, data]);
111
+ }
112
+
113
+ function encodeVarint(i) {
114
+ if (i < 253) {
115
+ return Buffer.from([i]);
116
+ } else if (i < 0x10000) {
117
+ const buf = Buffer.alloc(3);
118
+ buf.writeUInt8(0xfd, 0);
119
+ buf.writeUInt16LE(i, 1);
120
+ return buf;
121
+ } else if (i < 0x100000000) {
122
+ const buf = Buffer.alloc(5);
123
+ buf.writeUInt8(0xfe, 0);
124
+ buf.writeUInt32LE(i, 1);
125
+ return buf;
126
+ } else {
127
+ throw new Error(`Integer is too large: ${i}`);
128
+ }
129
+ }
130
+
131
+ function _tapBranchTaggedHash(a, b) {
132
+ return this.taggedHash(
133
+ "TapBranch",
134
+ Buffer.compare(a, b) < 0 ? Buffer.concat([a, b]) : Buffer.concat([b, a])
135
+ );
136
+ }
137
+
138
+ function calculatePrivateTweek(secret, tweek) {
139
+ let negatedKey = new BN(secret);
140
+ const publicKey = ec.g.mul(negatedKey);
141
+ if (publicKey.getY().isOdd()) {
142
+ negatedKey = ec.n.sub(negatedKey);
143
+ }
144
+ const tw = negatedKey.add(tweek).umod(ec.n);
145
+ return tw.toArrayLike(Buffer, "be", 32);
146
+ }
147
+
148
+ function sha256(data) {
149
+ return createHash("sha256").update(data).digest();
150
+ }
151
+
152
+ function taggedHash(data, tag) {
153
+ const tagDigest = sha256(Buffer.from(tag, "utf8"));
154
+ const concat = Buffer.concat([tagDigest, tagDigest, data]);
155
+ return sha256(concat);
156
+ }
157
+
158
+ function sha256(data) {
159
+ return createHash("sha256").update(data).digest();
160
+ }
161
+
162
+ module.exports = {
163
+ toBytes,
164
+ tweakMulPublic,
165
+ tweakAddPublic,
166
+ tweakAddPrivate,
167
+ tweakMulPrivate,
168
+ negate,
169
+ pubNegate,
170
+ toTweakedTaprootKey,
171
+ taggedHash,
172
+ _getTagHashedMerkleRoot,
173
+ _tapleafTaggedHash,
174
+ prependVarint,
175
+ encodeVarint,
176
+ };