Skip to content

Commit ddc0165

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add proper parsing of BitGo PSBT extensions
Add structured parsing for Musig2 PSBT extensions: - Participants data - Public nonces - Partial signatures These are now displayed in a human-readable format in the CLI tool instead of raw byte arrays. Issue: BTC-2652 Co-authored-by: llm-git <[email protected]>
1 parent 6d1e5ad commit ddc0165

File tree

2 files changed

+165
-37
lines changed

2 files changed

+165
-37
lines changed

packages/wasm-utxo/cli/src/parse/node.rs

Lines changed: 143 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ use bitcoin::consensus::Decodable;
33
use bitcoin::hashes::Hash;
44
use bitcoin::psbt::Psbt;
55
use bitcoin::{Network, ScriptBuf, Transaction};
6+
use wasm_utxo::bitgo_psbt::{
7+
BitGoKeyValue, Musig2PartialSig, Musig2Participants, Musig2PubNonce, ProprietaryKeySubtype,
8+
BITGO,
9+
};
610

711
pub use crate::node::{Node, Primitive};
812

@@ -39,24 +43,151 @@ fn bip32_derivations_to_nodes(
3943
.collect()
4044
}
4145

46+
fn musig2_participants_to_node(participants: &Musig2Participants) -> Node {
47+
let mut node = Node::new("musig2_participants", Primitive::None);
48+
node.add_child(Node::new(
49+
"tap_output_key",
50+
Primitive::Buffer(participants.tap_output_key.serialize().to_vec()),
51+
));
52+
node.add_child(Node::new(
53+
"tap_internal_key",
54+
Primitive::Buffer(participants.tap_internal_key.serialize().to_vec()),
55+
));
56+
57+
let mut participants_node = Node::new("participant_pub_keys", Primitive::U64(2));
58+
for (i, pub_key) in participants.participant_pub_keys.iter().enumerate() {
59+
let pub_key_vec: Vec<u8> = pub_key.to_bytes().to_vec();
60+
participants_node.add_child(Node::new(
61+
format!("participant_{}", i),
62+
Primitive::Buffer(pub_key_vec),
63+
));
64+
}
65+
node.add_child(participants_node);
66+
node
67+
}
68+
69+
fn musig2_pub_nonce_to_node(nonce: &Musig2PubNonce) -> Node {
70+
let mut node = Node::new("musig2_pub_nonce", Primitive::None);
71+
node.add_child(Node::new(
72+
"participant_pub_key",
73+
Primitive::Buffer(nonce.participant_pub_key.to_bytes().to_vec()),
74+
));
75+
node.add_child(Node::new(
76+
"tap_output_key",
77+
Primitive::Buffer(nonce.tap_output_key.serialize().to_vec()),
78+
));
79+
node.add_child(Node::new(
80+
"pub_nonce",
81+
Primitive::Buffer(nonce.pub_nonce.serialize().to_vec()),
82+
));
83+
node
84+
}
85+
86+
fn musig2_partial_sig_to_node(sig: &Musig2PartialSig) -> Node {
87+
let mut node = Node::new("musig2_partial_sig", Primitive::None);
88+
node.add_child(Node::new(
89+
"participant_pub_key",
90+
Primitive::Buffer(sig.participant_pub_key.to_bytes().to_vec()),
91+
));
92+
node.add_child(Node::new(
93+
"tap_output_key",
94+
Primitive::Buffer(sig.tap_output_key.serialize().to_vec()),
95+
));
96+
node.add_child(Node::new(
97+
"partial_sig",
98+
Primitive::Buffer(sig.partial_sig.clone()),
99+
));
100+
node
101+
}
102+
103+
fn bitgo_proprietary_to_node(prop_key: &bitcoin::psbt::raw::ProprietaryKey, v: &[u8]) -> Node {
104+
// Try to parse as BitGo key-value
105+
let v_vec = v.to_vec();
106+
let bitgo_kv_result = BitGoKeyValue::from_key_value(prop_key, &v_vec);
107+
108+
match bitgo_kv_result {
109+
Ok(bitgo_kv) => {
110+
// Parse based on subtype
111+
match bitgo_kv.subtype {
112+
ProprietaryKeySubtype::Musig2ParticipantPubKeys => {
113+
match Musig2Participants::from_key_value(&bitgo_kv) {
114+
Ok(participants) => musig2_participants_to_node(&participants),
115+
Err(_) => {
116+
// Fall back to raw display
117+
raw_proprietary_to_node("musig2_participants_error", prop_key, v)
118+
}
119+
}
120+
}
121+
ProprietaryKeySubtype::Musig2PubNonce => {
122+
match Musig2PubNonce::from_key_value(&bitgo_kv) {
123+
Ok(nonce) => musig2_pub_nonce_to_node(&nonce),
124+
Err(_) => {
125+
// Fall back to raw display
126+
raw_proprietary_to_node("musig2_pub_nonce_error", prop_key, v)
127+
}
128+
}
129+
}
130+
ProprietaryKeySubtype::Musig2PartialSig => {
131+
match Musig2PartialSig::from_key_value(&bitgo_kv) {
132+
Ok(sig) => musig2_partial_sig_to_node(&sig),
133+
Err(_) => {
134+
// Fall back to raw display
135+
raw_proprietary_to_node("musig2_partial_sig_error", prop_key, v)
136+
}
137+
}
138+
}
139+
_ => {
140+
// Other BitGo subtypes - show with name
141+
let subtype_name = match bitgo_kv.subtype {
142+
ProprietaryKeySubtype::ZecConsensusBranchId => "zec_consensus_branch_id",
143+
ProprietaryKeySubtype::PayGoAddressAttestationProof => {
144+
"paygo_address_attestation_proof"
145+
}
146+
ProprietaryKeySubtype::Bip322Message => "bip322_message",
147+
_ => "unknown",
148+
};
149+
raw_proprietary_to_node(subtype_name, prop_key, v)
150+
}
151+
}
152+
}
153+
Err(_) => {
154+
// Not a valid BitGo key-value, show raw
155+
raw_proprietary_to_node("unknown", prop_key, v)
156+
}
157+
}
158+
}
159+
160+
fn raw_proprietary_to_node(
161+
label: &str,
162+
prop_key: &bitcoin::psbt::raw::ProprietaryKey,
163+
v: &[u8],
164+
) -> Node {
165+
let mut prop_node = Node::new(label, Primitive::None);
166+
prop_node.add_child(Node::new(
167+
"prefix",
168+
Primitive::String(String::from_utf8_lossy(&prop_key.prefix).to_string()),
169+
));
170+
prop_node.add_child(Node::new("subtype", Primitive::U8(prop_key.subtype)));
171+
prop_node.add_child(Node::new(
172+
"key_data",
173+
Primitive::Buffer(prop_key.key.to_vec()),
174+
));
175+
prop_node.add_child(Node::new("value", Primitive::Buffer(v.to_vec())));
176+
prop_node
177+
}
178+
42179
fn proprietary_to_nodes(
43180
proprietary: &std::collections::BTreeMap<bitcoin::psbt::raw::ProprietaryKey, Vec<u8>>,
44181
) -> Vec<Node> {
45182
proprietary
46183
.iter()
47184
.map(|(prop_key, v)| {
48-
let mut prop_node = Node::new("key", Primitive::None);
49-
prop_node.add_child(Node::new(
50-
"prefix",
51-
Primitive::String(String::from_utf8_lossy(&prop_key.prefix).to_string()),
52-
));
53-
prop_node.add_child(Node::new("subtype", Primitive::U8(prop_key.subtype)));
54-
prop_node.add_child(Node::new(
55-
"key_data",
56-
Primitive::Buffer(prop_key.key.to_vec()),
57-
));
58-
prop_node.add_child(Node::new("value", Primitive::Buffer(v.to_vec())));
59-
prop_node
185+
// Check if this is a BITGO proprietary key
186+
if prop_key.prefix.as_slice() == BITGO {
187+
bitgo_proprietary_to_node(prop_key, v)
188+
} else {
189+
raw_proprietary_to_node("key", prop_key, v)
190+
}
60191
})
61192
.collect()
62193
}

packages/wasm-utxo/cli/test/fixtures/psbt_bitcoin_fullsigned.txt

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -182,31 +182,28 @@ psbt: None
182182
│ │ ├─ sighash_type: 0u32
183183
│ │ ├─ sighash_type: SIGHASH_DEFAULT
184184
│ │ └─ proprietary: 5u64
185-
│ │ ├─ key: None
186-
│ │ │ ├─ prefix: BITGO
187-
│ │ │ ├─ subtype: 1u8
188-
│ │ │ ├─ key_data: 15c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16eb5ad29a85aed24de2880e774caaf624f9cb1be09c67ed4aefbb9b7bc12ddf1a (64 bytes)
189-
│ │ │ └─ value: 021d978a17486ff9e47c82990269e531fc63981419d4ce73ee8bd2c99661c53953020fdea69e40a3adef3cdc7fa6f3af02f4c9d9e3254503c96a6a2b4aa66e778171 (66 bytes)
190-
│ │ ├─ key: None
191-
│ │ │ ├─ prefix: BITGO
192-
│ │ │ ├─ subtype: 2u8
193-
│ │ │ ├─ key_data: 020fdea69e40a3adef3cdc7fa6f3af02f4c9d9e3254503c96a6a2b4aa66e77817115c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (65 bytes)
194-
│ │ │ └─ value: 02826949740dff45d408b1f19d94c720f53411e02c525b28ab3c593b6b530fe0370374b8a0ffcaaaee6b772dac5f7c23ef33670b32ec77c6d41efb3c36df2165a094 (66 bytes)
195-
│ │ ├─ key: None
196-
│ │ │ ├─ prefix: BITGO
197-
│ │ │ ├─ subtype: 2u8
198-
│ │ │ ├─ key_data: 021d978a17486ff9e47c82990269e531fc63981419d4ce73ee8bd2c99661c5395315c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (65 bytes)
199-
│ │ │ └─ value: 03a4aaf46f3a0bc39a73855fa875b2f2f04bdb06235afbaedf857b593dddc63cb402cdb7f1a93ec526282198d834423371757e8f43932d03b9f43c3f7378300ae508 (66 bytes)
200-
│ │ ├─ key: None
201-
│ │ │ ├─ prefix: BITGO
202-
│ │ │ ├─ subtype: 3u8
203-
│ │ │ ├─ key_data: 020fdea69e40a3adef3cdc7fa6f3af02f4c9d9e3254503c96a6a2b4aa66e77817115c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (65 bytes)
204-
│ │ │ └─ value: fbdc39c3b8ffca4e6caf3298fa1a4be546b99163c2f21bd374d2254ec5b3c0f4 (32 bytes)
205-
│ │ └─ key: None
206-
│ │ ├─ prefix: BITGO
207-
│ │ ├─ subtype: 3u8
208-
│ │ ├─ key_data: 021d978a17486ff9e47c82990269e531fc63981419d4ce73ee8bd2c99661c5395315c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (65 bytes)
209-
│ │ └─ value: 1cd8a0c0598b0d88f889fdf9a3140f8f088e2187803a2faaee3f13c0163eb76c (32 bytes)
185+
│ │ ├─ musig2_participants: None
186+
│ │ │ ├─ tap_output_key: 15c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (32 bytes)
187+
│ │ │ ├─ tap_internal_key: eb5ad29a85aed24de2880e774caaf624f9cb1be09c67ed4aefbb9b7bc12ddf1a (32 bytes)
188+
│ │ │ └─ participant_pub_keys: 2u64
189+
│ │ │ ├─ participant_0: 021d978a17486ff9e47c82990269e531fc63981419d4ce73ee8bd2c99661c53953 (33 bytes)
190+
│ │ │ └─ participant_1: 020fdea69e40a3adef3cdc7fa6f3af02f4c9d9e3254503c96a6a2b4aa66e778171 (33 bytes)
191+
│ │ ├─ musig2_pub_nonce: None
192+
│ │ │ ├─ participant_pub_key: 020fdea69e40a3adef3cdc7fa6f3af02f4c9d9e3254503c96a6a2b4aa66e778171 (33 bytes)
193+
│ │ │ ├─ tap_output_key: 15c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (32 bytes)
194+
│ │ │ └─ pub_nonce: 02826949740dff45d408b1f19d94c720f53411e02c525b28ab3c593b6b530fe0370374b8a0ffcaaaee6b772dac5f7c23ef33670b32ec77c6d41efb3c36df2165a094 (66 bytes)
195+
│ │ ├─ musig2_pub_nonce: None
196+
│ │ │ ├─ participant_pub_key: 021d978a17486ff9e47c82990269e531fc63981419d4ce73ee8bd2c99661c53953 (33 bytes)
197+
│ │ │ ├─ tap_output_key: 15c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (32 bytes)
198+
│ │ │ └─ pub_nonce: 03a4aaf46f3a0bc39a73855fa875b2f2f04bdb06235afbaedf857b593dddc63cb402cdb7f1a93ec526282198d834423371757e8f43932d03b9f43c3f7378300ae508 (66 bytes)
199+
│ │ ├─ musig2_partial_sig: None
200+
│ │ │ ├─ participant_pub_key: 020fdea69e40a3adef3cdc7fa6f3af02f4c9d9e3254503c96a6a2b4aa66e778171 (33 bytes)
201+
│ │ │ ├─ tap_output_key: 15c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (32 bytes)
202+
│ │ │ └─ partial_sig: fbdc39c3b8ffca4e6caf3298fa1a4be546b99163c2f21bd374d2254ec5b3c0f4 (32 bytes)
203+
│ │ └─ musig2_partial_sig: None
204+
│ │ ├─ participant_pub_key: 021d978a17486ff9e47c82990269e531fc63981419d4ce73ee8bd2c99661c53953 (33 bytes)
205+
│ │ ├─ tap_output_key: 15c5815026f6a54b10194fc6980f1866a02d9ec128533c7997cdb4289bf3ef16 (32 bytes)
206+
│ │ └─ partial_sig: 1cd8a0c0598b0d88f889fdf9a3140f8f088e2187803a2faaee3f13c0163eb76c (32 bytes)
210207
│ └─ input_6: None
211208
│ ├─ non_witness_utxo: 97441d99a8d66f124ab3c9de26b87bd00aeb1547051c842a88165c1b089ee902 (32 bytes)
212209
│ ├─ redeem_script: 210336ef228ffe9b8efffba052c32d334660dd1f8366cf8fe44ae5aa672b6b629095ac (35 bytes)

0 commit comments

Comments
 (0)