-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpropkv.rs
More file actions
336 lines (292 loc) · 11 KB
/
propkv.rs
File metadata and controls
336 lines (292 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
//! Proprietary key-value utilities for PSBT fields
//!
//! This module provides utilities for working with proprietary key-values in PSBTs,
//! specifically for BitGo-specific extensions like MuSig2 data.
//! ```
pub use miniscript::bitcoin::psbt::raw::ProprietaryKey;
/// Find proprietary key-values in PSBT proprietary field matching the criteria
fn find_kv_iter<'a>(
map: &'a std::collections::BTreeMap<ProprietaryKey, Vec<u8>>,
prefix: &'a [u8],
subtype: Option<u8>,
) -> impl Iterator<Item = (&'a ProprietaryKey, &'a Vec<u8>)> + 'a {
map.iter().filter(move |(k, _)| {
// Check if the prefix matches
if k.prefix.as_slice() != prefix {
return false;
}
// Check if subtype matches (if specified)
if let Some(st) = subtype {
if k.subtype != st {
return false;
}
}
true
})
}
/// BitGo proprietary key identifier
pub const BITGO: &[u8] = b"BITGO";
/// Subtypes for proprietary keys that BitGo uses
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ProprietaryKeySubtype {
ZecConsensusBranchId = 0x00,
Musig2ParticipantPubKeys = 0x01,
Musig2PubNonce = 0x02,
Musig2PartialSig = 0x03,
PayGoAddressAttestationProof = 0x04,
Bip322Message = 0x05,
WasmUtxoSignedWith = 0x06,
}
impl ProprietaryKeySubtype {
pub fn from(value: u8) -> Option<Self> {
match value {
0x00 => Some(ProprietaryKeySubtype::ZecConsensusBranchId),
0x01 => Some(ProprietaryKeySubtype::Musig2ParticipantPubKeys),
0x02 => Some(ProprietaryKeySubtype::Musig2PubNonce),
0x03 => Some(ProprietaryKeySubtype::Musig2PartialSig),
0x04 => Some(ProprietaryKeySubtype::PayGoAddressAttestationProof),
0x05 => Some(ProprietaryKeySubtype::Bip322Message),
0x06 => Some(ProprietaryKeySubtype::WasmUtxoSignedWith),
_ => None,
}
}
}
#[derive(Debug)]
pub struct BitGoKeyValueError {
pub message: String,
}
pub struct BitGoKeyValue {
pub subtype: ProprietaryKeySubtype,
pub key: Vec<u8>,
pub value: Vec<u8>,
}
impl BitGoKeyValue {
pub fn new(subtype: ProprietaryKeySubtype, key: Vec<u8>, value: Vec<u8>) -> Self {
Self {
subtype,
key,
value,
}
}
pub fn from_key_value(key: &ProprietaryKey, value: &[u8]) -> Result<Self, BitGoKeyValueError> {
let subtype = ProprietaryKeySubtype::from(key.subtype);
match subtype {
Some(subtype) => Ok(Self::new(subtype, key.key.clone(), value.to_owned())),
None => Err(BitGoKeyValueError {
message: format!(
"Unknown or unsupported BitGo proprietary key subtype: {}",
key.subtype
),
}),
}
}
pub fn to_key_value(&self) -> (ProprietaryKey, Vec<u8>) {
let key = ProprietaryKey {
prefix: BITGO.to_vec(),
subtype: self.subtype as u8,
key: self.key.clone(),
};
(key, self.value.clone())
}
}
pub fn find_kv<'a>(
subtype: ProprietaryKeySubtype,
map: &'a std::collections::BTreeMap<ProprietaryKey, Vec<u8>>,
) -> impl Iterator<Item = BitGoKeyValue> + 'a {
find_kv_iter(map, BITGO, Some(subtype as u8)).map(|(key, value)| {
BitGoKeyValue::from_key_value(key, value).expect("Failed to create BitGoKeyValue")
})
}
/// Check if a proprietary key is a BitGo key
pub fn is_bitgo_key(key: &ProprietaryKey) -> bool {
key.prefix.as_slice() == BITGO
}
/// Check if a proprietary key is a BitGo MuSig2 key
pub fn is_musig2_key(key: &ProprietaryKey) -> bool {
if !is_bitgo_key(key) {
return false;
}
matches!(
ProprietaryKeySubtype::from(key.subtype),
Some(ProprietaryKeySubtype::Musig2ParticipantPubKeys)
| Some(ProprietaryKeySubtype::Musig2PubNonce)
| Some(ProprietaryKeySubtype::Musig2PartialSig)
)
}
/// Version information for wasm-utxo operations on PSBTs
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WasmUtxoVersionInfo {
pub version: String,
pub git_hash: String,
}
impl WasmUtxoVersionInfo {
/// Create a new version info structure
pub fn new(version: String, git_hash: String) -> Self {
Self { version, git_hash }
}
/// Get the version info from compile-time constants
/// Falls back to "unknown" if build.rs hasn't set the environment variables
pub fn from_build_info() -> Self {
Self {
version: option_env!("WASM_UTXO_VERSION")
.unwrap_or("unknown")
.to_string(),
git_hash: option_env!("WASM_UTXO_GIT_HASH")
.unwrap_or("unknown")
.to_string(),
}
}
/// Serialize to bytes for proprietary key-value storage
/// Format: <version_len: u8><version_bytes><git_hash_bytes (40 hex chars)>
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let version_bytes = self.version.as_bytes();
bytes.push(version_bytes.len() as u8);
bytes.extend_from_slice(version_bytes);
bytes.extend_from_slice(self.git_hash.as_bytes());
bytes
}
/// Deserialize from bytes
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
if bytes.is_empty() {
return Err("Empty version info bytes".to_string());
}
let version_len = bytes[0] as usize;
if bytes.len() < 1 + version_len {
return Err("Invalid version info: not enough bytes for version".to_string());
}
let version = String::from_utf8(bytes[1..1 + version_len].to_vec())
.map_err(|e| format!("Invalid UTF-8 in version: {}", e))?;
let git_hash = String::from_utf8(bytes[1 + version_len..].to_vec())
.map_err(|e| format!("Invalid UTF-8 in git hash: {}", e))?;
Ok(Self { version, git_hash })
}
/// Build a (ProprietaryKey, value) pair for per-input "signed-with" storage
pub fn build_key_value() -> (ProprietaryKey, Vec<u8>) {
BitGoKeyValue::new(
ProprietaryKeySubtype::WasmUtxoSignedWith,
vec![],
WasmUtxoVersionInfo::from_build_info().to_bytes(),
)
.to_key_value()
}
}
/// Extract Zcash consensus branch ID from PSBT global proprietary map.
///
/// The consensus branch ID is stored as a 4-byte little-endian u32 value
/// under the BitGo proprietary key with subtype `ZecConsensusBranchId` (0x00).
///
/// # Returns
/// - `Some(u32)` if the consensus branch ID is present and valid
/// - `None` if the key is not present or the value is malformed
pub fn get_zec_consensus_branch_id(psbt: &miniscript::bitcoin::psbt::Psbt) -> Option<u32> {
let kv = find_kv(
ProprietaryKeySubtype::ZecConsensusBranchId,
&psbt.proprietary,
)
.next()?;
if kv.value.len() == 4 {
let bytes: [u8; 4] = kv.value.as_slice().try_into().ok()?;
Some(u32::from_le_bytes(bytes))
} else {
None
}
}
/// Set Zcash consensus branch ID in PSBT global proprietary map.
///
/// The consensus branch ID is stored as a 4-byte little-endian u32 value
/// under the BitGo proprietary key with subtype `ZecConsensusBranchId` (0x00).
///
/// # Arguments
/// * `psbt` - The PSBT to modify
/// * `branch_id` - The Zcash consensus branch ID to store
///
/// # Example
/// ```ignore
/// use crate::zcash::NetworkUpgrade;
/// set_zec_consensus_branch_id(&mut psbt, NetworkUpgrade::Nu5.branch_id());
/// ```
///
/// See [`crate::zcash`] module for available network upgrades and their branch IDs.
pub fn set_zec_consensus_branch_id(psbt: &mut miniscript::bitcoin::psbt::Psbt, branch_id: u32) {
let kv = BitGoKeyValue::new(
ProprietaryKeySubtype::ZecConsensusBranchId,
vec![], // empty key
branch_id.to_le_bytes().to_vec(),
);
let (key, value) = kv.to_key_value();
psbt.proprietary.insert(key, value);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proprietary_key_structure() {
let key = ProprietaryKey {
prefix: b"BITGO".to_vec(),
subtype: 0x03,
key: vec![1, 2, 3],
};
assert_eq!(key.prefix, b"BITGO");
assert_eq!(key.subtype, 0x03);
assert_eq!(key.key, vec![1, 2, 3]);
}
#[test]
fn test_zec_consensus_branch_id_roundtrip() {
use crate::zcash::NetworkUpgrade;
use miniscript::bitcoin::psbt::Psbt;
use miniscript::bitcoin::Transaction;
// Create a minimal PSBT
let tx = Transaction {
version: miniscript::bitcoin::transaction::Version::TWO,
lock_time: miniscript::bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![],
output: vec![],
};
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();
// Initially no branch ID
assert_eq!(get_zec_consensus_branch_id(&psbt), None);
// Set NU5 branch ID using generated constant
let nu5_branch_id = NetworkUpgrade::Nu5.branch_id();
set_zec_consensus_branch_id(&mut psbt, nu5_branch_id);
// Should be retrievable
assert_eq!(get_zec_consensus_branch_id(&psbt), Some(nu5_branch_id));
// Update to Sapling branch ID using generated constant
let sapling_branch_id = NetworkUpgrade::Sapling.branch_id();
set_zec_consensus_branch_id(&mut psbt, sapling_branch_id);
// Should return the updated value
assert_eq!(get_zec_consensus_branch_id(&psbt), Some(sapling_branch_id));
}
#[test]
fn test_zec_consensus_branch_id_values() {
use crate::zcash::NetworkUpgrade;
// Verify known Zcash branch IDs match expected values from ZIP-200
assert_eq!(NetworkUpgrade::Overwinter.branch_id(), 0x5ba81b19);
assert_eq!(NetworkUpgrade::Sapling.branch_id(), 0x76b809bb);
assert_eq!(NetworkUpgrade::Blossom.branch_id(), 0x2bb40e60);
assert_eq!(NetworkUpgrade::Heartwood.branch_id(), 0xf5b9230b);
assert_eq!(NetworkUpgrade::Canopy.branch_id(), 0xe9ff75a6);
assert_eq!(NetworkUpgrade::Nu5.branch_id(), 0xc2d6d0b4);
assert_eq!(NetworkUpgrade::Nu6.branch_id(), 0xc8e71055);
}
#[test]
fn test_version_info_serialization() {
let version_info =
WasmUtxoVersionInfo::new("0.0.2".to_string(), "abc123def456".to_string());
let bytes = version_info.to_bytes();
let deserialized = WasmUtxoVersionInfo::from_bytes(&bytes).unwrap();
assert_eq!(deserialized, version_info);
}
#[test]
fn test_version_info_build_key_value() {
let (key, value) = WasmUtxoVersionInfo::build_key_value();
assert_eq!(key.prefix, b"BITGO");
assert_eq!(key.subtype, ProprietaryKeySubtype::WasmUtxoSignedWith as u8);
let empty_vec: Vec<u8> = vec![];
assert_eq!(key.key, empty_vec);
// The value should round-trip through from_bytes
let info = WasmUtxoVersionInfo::from_bytes(&value).unwrap();
assert_eq!(info, WasmUtxoVersionInfo::from_build_info());
}
}