@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 +200 -0
- package/classes/CreateOutput.js +337 -0
- package/classes/KeyGeneration.js +195 -0
- package/images/bitcoin.png +0 -0
- package/images/silent.png +0 -0
- package/index.js +23 -0
- package/package.json +24 -0
- package/utils/bech32.js +171 -0
- package/utils/const.js +2 -0
- package/utils/info.js +9 -0
- package/utils/network.js +9 -0
- package/utils/output.js +9 -0
- package/utils/scriptOutput.js +8 -0
- package/utils/taproot.js +102 -0
- package/utils/utils.js +176 -0
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
|
+
}
|
package/utils/bech32.js
ADDED
@@ -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
package/utils/info.js
ADDED
package/utils/network.js
ADDED
package/utils/output.js
ADDED
package/utils/taproot.js
ADDED
@@ -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
|
+
};
|