@mysten-incubation/hashi 0.3.0 → 0.3.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/CHANGELOG.md +6 -0
- package/dist/bitcoin.d.mts +15 -8
- package/dist/bitcoin.mjs +71 -27
- package/dist/client.d.mts +4 -4
- package/dist/client.mjs +4 -4
- package/dist/types.d.mts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dist/bitcoin.d.mts
CHANGED
|
@@ -56,14 +56,14 @@ declare function arkworksToSec1Compressed(ark: Uint8Array): Uint8Array;
|
|
|
56
56
|
*/
|
|
57
57
|
declare function deriveChildPubkey(mpcKeyCompressed: Uint8Array, suiAddress: Uint8Array): Uint8Array;
|
|
58
58
|
/**
|
|
59
|
-
* Builds
|
|
60
|
-
* `tr(NUMS, multi_a(2, guardian, derived_mpc))`.
|
|
59
|
+
* Builds Hashi's P2TR script-path-only deposit address:
|
|
60
|
+
* `tr(NUMS, {multi_a(2, guardian, derived_mpc), and_v(v:older(delay), pk(derived_mpc))})`.
|
|
61
61
|
*
|
|
62
62
|
* The leaf script is a BIP-342 `multi_a` 2-of-2 — both Schnorr signatures must
|
|
63
63
|
* be present to spend, ordered so that the witness stack is
|
|
64
64
|
* `[derived_mpc_sig, guardian_sig, leaf_script, control_block]` (LIFO). This
|
|
65
|
-
* is the exact script the bridge's withdrawal path constructs
|
|
66
|
-
* `crates/hashi-types/src/
|
|
65
|
+
* is the exact immediate-spend script the bridge's withdrawal path constructs
|
|
66
|
+
* in `crates/hashi-types/src/bitcoin/taproot.rs`'s `compute_taproot_descriptor`.
|
|
67
67
|
*
|
|
68
68
|
* The leaf script is exactly 70 bytes:
|
|
69
69
|
*
|
|
@@ -80,11 +80,17 @@ declare function deriveChildPubkey(mpcKeyCompressed: Uint8Array, suiAddress: Uin
|
|
|
80
80
|
* codegen routes through `Builder::push_int(2)` which emits the small-num
|
|
81
81
|
* opcode. A literal pushdata would change the leaf hash and the address.
|
|
82
82
|
*
|
|
83
|
+
* The recovery leaf script is a BIP-342 `and_v(v:older(delay), pk(derived_mpc))`:
|
|
84
|
+
* after Hashi's 60-day BIP-68 relative timelock, the MPC child key alone can
|
|
85
|
+
* spend the output if the guardian key is unavailable.
|
|
86
|
+
*
|
|
83
87
|
* The taproot output key is computed per BIP-341:
|
|
84
88
|
*
|
|
85
89
|
* ```text
|
|
86
|
-
*
|
|
87
|
-
*
|
|
90
|
+
* leaf1 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(two_of_two_script) ‖ two_of_two_script)
|
|
91
|
+
* leaf2 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(recovery_script) ‖ recovery_script)
|
|
92
|
+
* root = tagged_hash("TapBranch", min(leaf1, leaf2) ‖ max(leaf1, leaf2))
|
|
93
|
+
* tweak = tagged_hash("TapTweak", NUMS ‖ root)
|
|
88
94
|
* outputKey = NUMS + tweak × G
|
|
89
95
|
* ```
|
|
90
96
|
*
|
|
@@ -128,11 +134,12 @@ interface DepositAddressInputs {
|
|
|
128
134
|
* Main entry point for the address derivation pipeline. Combines
|
|
129
135
|
* {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
|
|
130
136
|
* a single call. The produced address matches the Rust node's
|
|
131
|
-
* `
|
|
137
|
+
* `hashi_types::bitcoin::taproot::taproot_address` byte-for-byte.
|
|
132
138
|
*
|
|
133
139
|
* The address scheme is:
|
|
134
140
|
* ```text
|
|
135
|
-
* tr(NUMS, multi_a(2, guardian, derive(mpc_master, sui_address))
|
|
141
|
+
* tr(NUMS, {multi_a(2, guardian, derive(mpc_master, sui_address)),
|
|
142
|
+
* and_v(v:older(delay), pk(derive(mpc_master, sui_address)))})
|
|
136
143
|
* ```
|
|
137
144
|
*
|
|
138
145
|
* @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
|
package/dist/bitcoin.mjs
CHANGED
|
@@ -16,10 +16,10 @@ import { concatBytes } from "@noble/hashes/utils.js";
|
|
|
16
16
|
* and the Hashi guardian co-sign the withdrawal that spends it, and the bridge
|
|
17
17
|
* mints equivalent tokens on Sui.
|
|
18
18
|
*
|
|
19
|
-
* The deposit address is a
|
|
20
|
-
*
|
|
21
|
-
* derive(mpc_master, sui_address))`
|
|
22
|
-
*
|
|
19
|
+
* The deposit address is a Pay-to-Taproot (P2TR / BIP-341) script-path address
|
|
20
|
+
* with two leaves: an immediate `multi_a(2, guardian_btc_pubkey,
|
|
21
|
+
* derive(mpc_master, sui_address))` spend, and a delayed MPC-only recovery
|
|
22
|
+
* spend after Hashi's BIP-68 relative timelock.
|
|
23
23
|
*
|
|
24
24
|
* The full derivation pipeline is:
|
|
25
25
|
*
|
|
@@ -34,23 +34,24 @@ import { concatBytes } from "@noble/hashes/utils.js";
|
|
|
34
34
|
* (see {@link deriveChildPubkey}). This replicates the Rust function
|
|
35
35
|
* `fastcrypto_tbls::threshold_schnorr::key_derivation::derive_verifying_key`.
|
|
36
36
|
*
|
|
37
|
-
* 3. **Build** the taproot address:
|
|
37
|
+
* 3. **Build** the taproot address:
|
|
38
|
+
* `tr(NUMS, {multi_a(2, guardian, child), and_v(v:older(delay), pk(child))})`
|
|
38
39
|
* where NUMS is a Nothing-Up-My-Sleeve point with no known private key,
|
|
39
40
|
* forcing all spends through the script path (see
|
|
40
41
|
* {@link twoOfTwoTaprootScriptPathAddress}).
|
|
41
42
|
*
|
|
42
43
|
* The end-to-end helper {@link generateDepositAddress} combines steps 2–3.
|
|
43
44
|
*
|
|
44
|
-
* Mirrors `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `cross_lang_2of2_test_vectors`.
|
|
45
|
+
* Mirrors `taproot_address` in `crates/hashi-types/src/bitcoin/taproot.rs`.
|
|
46
|
+
* Cross-language test vectors live in both this file's unit tests and the
|
|
47
|
+
* matching Rust unit test `cross_lang_2of2_test_vectors`.
|
|
48
48
|
*
|
|
49
49
|
* @see https://mystenlabs.github.io/hashi/design/address-scheme.html
|
|
50
50
|
*/
|
|
51
51
|
const Point = secp256k1.Point;
|
|
52
52
|
const CURVE_ORDER = Point.CURVE().n;
|
|
53
53
|
const NUMS_POINT = Point.fromBytes(concatBytes(new Uint8Array([2]), NUMS_KEY));
|
|
54
|
+
const HASHI_MPC_RECOVERY_DELAY_SEQUENCE = 1 << 22 | Math.ceil(1440 * 60 * 60 / 512);
|
|
54
55
|
/** BIP-340 tagged hash: SHA256(SHA256(tag) ‖ SHA256(tag) ‖ msg) */
|
|
55
56
|
function taggedHash(tag, ...msgs) {
|
|
56
57
|
const tagHash = sha256(new TextEncoder().encode(tag));
|
|
@@ -62,6 +63,37 @@ function bytesToNumberBE(bytes) {
|
|
|
62
63
|
for (const byte of bytes) n = n << 8n | BigInt(byte);
|
|
63
64
|
return n;
|
|
64
65
|
}
|
|
66
|
+
/** CompactSize for scripts small enough to fit in one byte. */
|
|
67
|
+
function compactSize(len) {
|
|
68
|
+
if (len >= 253) throw new Error(`Unsupported script length ${len}`);
|
|
69
|
+
return new Uint8Array([len]);
|
|
70
|
+
}
|
|
71
|
+
function scriptNum(n) {
|
|
72
|
+
if (n === 0) return new Uint8Array();
|
|
73
|
+
const bytes = [];
|
|
74
|
+
let value = n;
|
|
75
|
+
while (value > 0) {
|
|
76
|
+
bytes.push(value & 255);
|
|
77
|
+
value >>= 8;
|
|
78
|
+
}
|
|
79
|
+
if ((bytes[bytes.length - 1] & 128) !== 0) bytes.push(0);
|
|
80
|
+
return new Uint8Array(bytes);
|
|
81
|
+
}
|
|
82
|
+
function pushBytes(bytes) {
|
|
83
|
+
if (bytes.length >= 76) throw new Error(`Unsupported push length ${bytes.length}`);
|
|
84
|
+
return concatBytes(new Uint8Array([bytes.length]), bytes);
|
|
85
|
+
}
|
|
86
|
+
function lexicographicCompare(a, b) {
|
|
87
|
+
const len = Math.min(a.length, b.length);
|
|
88
|
+
for (let i = 0; i < len; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
89
|
+
return a.length - b.length;
|
|
90
|
+
}
|
|
91
|
+
function tapLeafHash(script) {
|
|
92
|
+
return taggedHash("TapLeaf", new Uint8Array([192]), compactSize(script.length), script);
|
|
93
|
+
}
|
|
94
|
+
function tapBranchHash(a, b) {
|
|
95
|
+
return lexicographicCompare(a, b) <= 0 ? taggedHash("TapBranch", a, b) : taggedHash("TapBranch", b, a);
|
|
96
|
+
}
|
|
65
97
|
/**
|
|
66
98
|
* Converts a 33-byte arkworks-compressed secp256k1 point to 33-byte SEC1 compressed format.
|
|
67
99
|
*
|
|
@@ -136,14 +168,14 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
|
|
|
136
168
|
return parentPoint.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
|
|
137
169
|
}
|
|
138
170
|
/**
|
|
139
|
-
* Builds
|
|
140
|
-
* `tr(NUMS, multi_a(2, guardian, derived_mpc))`.
|
|
171
|
+
* Builds Hashi's P2TR script-path-only deposit address:
|
|
172
|
+
* `tr(NUMS, {multi_a(2, guardian, derived_mpc), and_v(v:older(delay), pk(derived_mpc))})`.
|
|
141
173
|
*
|
|
142
174
|
* The leaf script is a BIP-342 `multi_a` 2-of-2 — both Schnorr signatures must
|
|
143
175
|
* be present to spend, ordered so that the witness stack is
|
|
144
176
|
* `[derived_mpc_sig, guardian_sig, leaf_script, control_block]` (LIFO). This
|
|
145
|
-
* is the exact script the bridge's withdrawal path constructs
|
|
146
|
-
* `crates/hashi-types/src/
|
|
177
|
+
* is the exact immediate-spend script the bridge's withdrawal path constructs
|
|
178
|
+
* in `crates/hashi-types/src/bitcoin/taproot.rs`'s `compute_taproot_descriptor`.
|
|
147
179
|
*
|
|
148
180
|
* The leaf script is exactly 70 bytes:
|
|
149
181
|
*
|
|
@@ -160,11 +192,17 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
|
|
|
160
192
|
* codegen routes through `Builder::push_int(2)` which emits the small-num
|
|
161
193
|
* opcode. A literal pushdata would change the leaf hash and the address.
|
|
162
194
|
*
|
|
195
|
+
* The recovery leaf script is a BIP-342 `and_v(v:older(delay), pk(derived_mpc))`:
|
|
196
|
+
* after Hashi's 60-day BIP-68 relative timelock, the MPC child key alone can
|
|
197
|
+
* spend the output if the guardian key is unavailable.
|
|
198
|
+
*
|
|
163
199
|
* The taproot output key is computed per BIP-341:
|
|
164
200
|
*
|
|
165
201
|
* ```text
|
|
166
|
-
*
|
|
167
|
-
*
|
|
202
|
+
* leaf1 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(two_of_two_script) ‖ two_of_two_script)
|
|
203
|
+
* leaf2 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(recovery_script) ‖ recovery_script)
|
|
204
|
+
* root = tagged_hash("TapBranch", min(leaf1, leaf2) ‖ max(leaf1, leaf2))
|
|
205
|
+
* tweak = tagged_hash("TapTweak", NUMS ‖ root)
|
|
168
206
|
* outputKey = NUMS + tweak × G
|
|
169
207
|
* ```
|
|
170
208
|
*
|
|
@@ -182,16 +220,21 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
|
|
|
182
220
|
function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, derivedMpcXOnly, network) {
|
|
183
221
|
if (guardianBtcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only guardian pubkey, got ${guardianBtcXOnly.length}`);
|
|
184
222
|
if (derivedMpcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only derived MPC pubkey, got ${derivedMpcXOnly.length}`);
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
223
|
+
const twoOfTwoScript = new Uint8Array(70);
|
|
224
|
+
twoOfTwoScript[0] = 32;
|
|
225
|
+
twoOfTwoScript.set(guardianBtcXOnly, 1);
|
|
226
|
+
twoOfTwoScript[33] = 172;
|
|
227
|
+
twoOfTwoScript[34] = 32;
|
|
228
|
+
twoOfTwoScript.set(derivedMpcXOnly, 35);
|
|
229
|
+
twoOfTwoScript[67] = 186;
|
|
230
|
+
twoOfTwoScript[68] = 82;
|
|
231
|
+
twoOfTwoScript[69] = 156;
|
|
232
|
+
const recoveryScript = concatBytes(pushBytes(scriptNum(HASHI_MPC_RECOVERY_DELAY_SEQUENCE)), new Uint8Array([
|
|
233
|
+
178,
|
|
234
|
+
105,
|
|
235
|
+
32
|
|
236
|
+
]), derivedMpcXOnly, new Uint8Array([172]));
|
|
237
|
+
const tweakScalar = bytesToNumberBE(taggedHash("TapTweak", NUMS_KEY, tapBranchHash(tapLeafHash(twoOfTwoScript), tapLeafHash(recoveryScript)))) % CURVE_ORDER;
|
|
195
238
|
const outputKey = NUMS_POINT.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
|
|
196
239
|
const words = [1, ...bech32m.toWords(outputKey)];
|
|
197
240
|
return bech32m.encode(NETWORK_HRP[network], words);
|
|
@@ -202,11 +245,12 @@ function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, derivedMpcXOnly, net
|
|
|
202
245
|
* Main entry point for the address derivation pipeline. Combines
|
|
203
246
|
* {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
|
|
204
247
|
* a single call. The produced address matches the Rust node's
|
|
205
|
-
* `
|
|
248
|
+
* `hashi_types::bitcoin::taproot::taproot_address` byte-for-byte.
|
|
206
249
|
*
|
|
207
250
|
* The address scheme is:
|
|
208
251
|
* ```text
|
|
209
|
-
* tr(NUMS, multi_a(2, guardian, derive(mpc_master, sui_address))
|
|
252
|
+
* tr(NUMS, {multi_a(2, guardian, derive(mpc_master, sui_address)),
|
|
253
|
+
* and_v(v:older(delay), pk(derive(mpc_master, sui_address)))})
|
|
210
254
|
* ```
|
|
211
255
|
*
|
|
212
256
|
* @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
|
package/dist/client.d.mts
CHANGED
|
@@ -52,10 +52,10 @@ declare class HashiClient {
|
|
|
52
52
|
*
|
|
53
53
|
* Fetches the MPC committee public key and the guardian's BTC public key
|
|
54
54
|
* from on-chain, derives an MPC child key against the Sui address, and
|
|
55
|
-
* builds
|
|
56
|
-
* (`
|
|
57
|
-
* the bridge's on-chain
|
|
58
|
-
* byte-for-byte.
|
|
55
|
+
* builds the Hashi taproot script tree: an immediate 2-of-2 leaf
|
|
56
|
+
* (`multi_a(2, guardian, derived_mpc)`) plus a delayed MPC-only recovery
|
|
57
|
+
* leaf. The address matches the bridge's on-chain
|
|
58
|
+
* `validate_deposit_request_derivation_path` check byte-for-byte.
|
|
59
59
|
*
|
|
60
60
|
* The MPC key (`committee_set.mpc_public_key`) and the guardian key
|
|
61
61
|
* (`guardian_btc_public_key` config) come from a single fetch of the
|
package/dist/client.mjs
CHANGED
|
@@ -444,10 +444,10 @@ var HashiClient = class {
|
|
|
444
444
|
*
|
|
445
445
|
* Fetches the MPC committee public key and the guardian's BTC public key
|
|
446
446
|
* from on-chain, derives an MPC child key against the Sui address, and
|
|
447
|
-
* builds
|
|
448
|
-
* (`
|
|
449
|
-
* the bridge's on-chain
|
|
450
|
-
* byte-for-byte.
|
|
447
|
+
* builds the Hashi taproot script tree: an immediate 2-of-2 leaf
|
|
448
|
+
* (`multi_a(2, guardian, derived_mpc)`) plus a delayed MPC-only recovery
|
|
449
|
+
* leaf. The address matches the bridge's on-chain
|
|
450
|
+
* `validate_deposit_request_derivation_path` check byte-for-byte.
|
|
451
451
|
*
|
|
452
452
|
* The MPC key (`committee_set.mpc_public_key`) and the guardian key
|
|
453
453
|
* (`guardian_btc_public_key` config) come from a single fetch of the
|
package/dist/types.d.mts
CHANGED
|
@@ -53,8 +53,8 @@ interface GovernanceConfig {
|
|
|
53
53
|
readonly guardianPublicKey: Uint8Array | null;
|
|
54
54
|
/**
|
|
55
55
|
* Guardian's BIP-340 x-only secp256k1 BTC public key (32 bytes), the
|
|
56
|
-
* `pk1` slot of the
|
|
57
|
-
* if unset (deposit-address derivation will fail).
|
|
56
|
+
* `pk1` slot of the immediate 2-of-2 leaf in the on-chain deposit-address
|
|
57
|
+
* descriptor. `null` if unset (deposit-address derivation will fail).
|
|
58
58
|
*/
|
|
59
59
|
readonly guardianBtcPublicKey: Uint8Array | null;
|
|
60
60
|
}
|