Skip to content

Commit 21341be

Browse files
committed
Replace Vec<u8> with type-safe representations for silent payment fields
- Use bitcoin::CompressedPublicKey for scan key and ECDH shares - Use custom DleqProof type for DLEQ proofs
1 parent d8013ad commit 21341be

File tree

8 files changed

+287
-91
lines changed

8 files changed

+287
-91
lines changed

src/macros.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,42 @@ macro_rules! v2_impl_psbt_hash_serialize {
128128
}
129129
};
130130
}
131+
132+
/// Macro for inserting BIP-375 silent payment fields with CompressedPublicKey keys.
133+
#[cfg(feature = "silent-payments")]
134+
macro_rules! v2_impl_psbt_insert_sp_pair {
135+
// For CompressedPublicKey values (ECDH shares)
136+
($map:expr, $raw_key:expr, $raw_value:expr, compressed_pubkey) => {{
137+
if $raw_key.key.is_empty() {
138+
return Err(InsertPairError::InvalidKeyDataEmpty($raw_key).into());
139+
}
140+
let scan_key = bitcoin::CompressedPublicKey::from_slice(&$raw_key.key)
141+
.map_err(|_| InsertPairError::KeyWrongLength($raw_key.key.len(), 33))?;
142+
let value = bitcoin::CompressedPublicKey::from_slice(&$raw_value)
143+
.map_err(|_| InsertPairError::ValueWrongLength($raw_value.len(), 33))?;
144+
match $map.entry(scan_key) {
145+
$crate::prelude::btree_map::Entry::Vacant(empty_key) => {
146+
empty_key.insert(value);
147+
}
148+
$crate::prelude::btree_map::Entry::Occupied(_) =>
149+
return Err(InsertPairError::DuplicateKey($raw_key).into()),
150+
}
151+
}};
152+
// For DleqProof values (DLEQ proofs)
153+
($map:expr, $raw_key:expr, $raw_value:expr, dleq_proof) => {{
154+
if $raw_key.key.is_empty() {
155+
return Err(InsertPairError::InvalidKeyDataEmpty($raw_key).into());
156+
}
157+
let scan_key = bitcoin::CompressedPublicKey::from_slice(&$raw_key.key)
158+
.map_err(|_| InsertPairError::KeyWrongLength($raw_key.key.len(), 33))?;
159+
let value = $crate::v2::dleq::DleqProof::try_from($raw_value.as_slice())
160+
.map_err(|_| InsertPairError::ValueWrongLength($raw_value.len(), 64))?;
161+
match $map.entry(scan_key) {
162+
$crate::prelude::btree_map::Entry::Vacant(empty_key) => {
163+
empty_key.insert(value);
164+
}
165+
$crate::prelude::btree_map::Entry::Occupied(_) =>
166+
return Err(InsertPairError::DuplicateKey($raw_key).into()),
167+
}
168+
}};
169+
}

src/serialize.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,27 @@ pub enum Error {
413413
LockTime(absolute::ConversionError),
414414
/// Unsupported PSBT version.
415415
UnsupportedVersion(version::UnsupportedVersionError),
416+
/// Invalid scan key for BIP-375 silent payments (expected 33 bytes).
417+
InvalidScanKey {
418+
/// The length that was provided.
419+
got: usize,
420+
/// The expected length.
421+
expected: usize,
422+
},
423+
/// Invalid ECDH share for BIP-375 silent payments (expected 33 bytes).
424+
InvalidEcdhShare {
425+
/// The length that was provided.
426+
got: usize,
427+
/// The expected length.
428+
expected: usize,
429+
},
430+
/// Invalid DLEQ proof for BIP-375 silent payments (expected 64 bytes).
431+
InvalidDleqProof {
432+
/// The length that was provided.
433+
got: usize,
434+
/// The expected length.
435+
expected: usize,
436+
},
416437
}
417438

418439
impl fmt::Display for Error {
@@ -440,6 +461,15 @@ impl fmt::Display for Error {
440461
f.write_str("data not consumed entirely when explicitly deserializing"),
441462
LockTime(ref e) => write_err!(f, "parsed locktime invalid"; e),
442463
UnsupportedVersion(ref e) => write_err!(f, "unsupported version"; e),
464+
InvalidScanKey { got, expected } => {
465+
write!(f, "invalid scan key: got {} bytes, expected {}", got, expected)
466+
}
467+
InvalidEcdhShare { got, expected } => {
468+
write!(f, "invalid ECDH share: got {} bytes, expected {}", got, expected)
469+
}
470+
InvalidDleqProof { got, expected } => {
471+
write!(f, "invalid DLEQ proof: got {} bytes, expected {}", got, expected)
472+
}
443473
}
444474
}
445475
}
@@ -467,7 +497,10 @@ impl std::error::Error for Error {
467497
| InvalidLeafVersion
468498
| Taproot(_)
469499
| TapTree(_)
470-
| PartialDataConsumption => None,
500+
| PartialDataConsumption
501+
| InvalidScanKey { .. }
502+
| InvalidEcdhShare { .. }
503+
| InvalidDleqProof { .. } => None,
471504
}
472505
}
473506
}

src/v2/dleq.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! BIP-375: Support for silent payments in PSBTs.
4+
//!
5+
//! This module provides type-safe wrapper for BIP-374 dleq proof field.
6+
7+
use core::fmt;
8+
9+
use crate::prelude::*;
10+
use crate::serialize::{Deserialize, Serialize};
11+
12+
/// A 64-byte DLEQ proof (BIP-374).
13+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14+
pub struct DleqProof(pub [u8; 64]);
15+
16+
#[cfg(feature = "serde")]
17+
impl actual_serde::Serialize for DleqProof {
18+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
19+
where
20+
S: actual_serde::Serializer,
21+
{
22+
if serializer.is_human_readable() {
23+
serializer.serialize_str(&bitcoin::hex::DisplayHex::to_lower_hex_string(&self.0[..]))
24+
} else {
25+
serializer.serialize_bytes(&self.0[..])
26+
}
27+
}
28+
}
29+
30+
#[cfg(feature = "serde")]
31+
impl<'de> actual_serde::Deserialize<'de> for DleqProof {
32+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
33+
where
34+
D: actual_serde::Deserializer<'de>,
35+
{
36+
if deserializer.is_human_readable() {
37+
struct HexVisitor;
38+
impl<'de> actual_serde::de::Visitor<'de> for HexVisitor {
39+
type Value = DleqProof;
40+
41+
fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
42+
f.write_str("a 64-byte hex string")
43+
}
44+
45+
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
46+
where
47+
E: actual_serde::de::Error,
48+
{
49+
use bitcoin::hex::FromHex;
50+
let vec = Vec::<u8>::from_hex(s).map_err(E::custom)?;
51+
DleqProof::try_from(vec).map_err(|e| {
52+
E::custom(format!("expected {} bytes, got {}", e.expected, e.got))
53+
})
54+
}
55+
}
56+
deserializer.deserialize_str(HexVisitor)
57+
} else {
58+
struct BytesVisitor;
59+
impl<'de> actual_serde::de::Visitor<'de> for BytesVisitor {
60+
type Value = DleqProof;
61+
62+
fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
63+
f.write_str("64 bytes")
64+
}
65+
66+
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
67+
where
68+
E: actual_serde::de::Error,
69+
{
70+
DleqProof::try_from(v).map_err(|e| {
71+
E::custom(format!("expected {} bytes, got {}", e.expected, e.got))
72+
})
73+
}
74+
}
75+
deserializer.deserialize_bytes(BytesVisitor)
76+
}
77+
}
78+
}
79+
80+
impl DleqProof {
81+
/// Creates a new [`DleqProof`] from a 64-byte array.
82+
pub fn new(bytes: [u8; 64]) -> Self { DleqProof(bytes) }
83+
84+
/// Returns the inner 64-byte array.
85+
pub fn as_bytes(&self) -> &[u8; 64] { &self.0 }
86+
}
87+
88+
impl From<[u8; 64]> for DleqProof {
89+
fn from(bytes: [u8; 64]) -> Self { DleqProof(bytes) }
90+
}
91+
92+
impl AsRef<[u8]> for DleqProof {
93+
fn as_ref(&self) -> &[u8] { &self.0 }
94+
}
95+
96+
impl TryFrom<&[u8]> for DleqProof {
97+
type Error = InvalidLengthError;
98+
99+
fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
100+
<[u8; 64]>::try_from(slice)
101+
.map(DleqProof)
102+
.map_err(|_| InvalidLengthError { got: slice.len(), expected: 64 })
103+
}
104+
}
105+
106+
impl TryFrom<Vec<u8>> for DleqProof {
107+
type Error = InvalidLengthError;
108+
109+
fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> { Self::try_from(v.as_slice()) }
110+
}
111+
112+
impl Serialize for DleqProof {
113+
fn serialize(&self) -> Vec<u8> { self.0.to_vec() }
114+
}
115+
116+
impl Deserialize for DleqProof {
117+
fn deserialize(bytes: &[u8]) -> Result<Self, crate::serialize::Error> {
118+
DleqProof::try_from(bytes).map_err(|e| crate::serialize::Error::InvalidDleqProof {
119+
got: e.got,
120+
expected: e.expected,
121+
})
122+
}
123+
}
124+
125+
/// Error returned when a byte array has an invalid length for a dleq proof.
126+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127+
pub struct InvalidLengthError {
128+
/// The length that was provided.
129+
pub got: usize,
130+
/// The expected length.
131+
pub expected: usize,
132+
}
133+
134+
impl fmt::Display for InvalidLengthError {
135+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136+
write!(f, "invalid length for BIP-375 type: got {}, expected {}", self.got, self.expected)
137+
}
138+
}
139+
140+
#[cfg(feature = "std")]
141+
impl std::error::Error for InvalidLengthError {}

src/v2/map/global.rs

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use core::fmt;
66
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub};
77
use bitcoin::consensus::{encode as consensus, Decodable};
88
use bitcoin::locktime::absolute;
9+
#[cfg(feature = "silent-payments")]
10+
use bitcoin::CompressedPublicKey;
911
use bitcoin::{bip32, transaction, VarInt};
1012

1113
use crate::consts::{
@@ -19,6 +21,8 @@ use crate::error::{write_err, InconsistentKeySourcesError};
1921
use crate::io::{BufRead, Cursor, Read};
2022
use crate::prelude::*;
2123
use crate::serialize::Serialize;
24+
#[cfg(feature = "silent-payments")]
25+
use crate::v2::dleq::DleqProof;
2226
use crate::v2::map::Map;
2327
use crate::version::Version;
2428
use crate::{consts, raw, serialize, V2};
@@ -60,13 +64,13 @@ pub struct Global {
6064

6165
/// BIP-375: Map from scan public key to ECDH share (33 bytes each).
6266
#[cfg(feature = "silent-payments")]
63-
#[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::btreemap_as_seq_byte_values"))]
64-
pub sp_ecdh_shares: BTreeMap<Vec<u8>, Vec<u8>>,
67+
#[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::btreemap_as_seq"))]
68+
pub sp_ecdh_shares: BTreeMap<CompressedPublicKey, CompressedPublicKey>,
6569

6670
/// BIP-375: Map from scan public key to DLEQ proof (64 bytes each).
6771
#[cfg(feature = "silent-payments")]
68-
#[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::btreemap_as_seq_byte_values"))]
69-
pub sp_dleq_proofs: BTreeMap<Vec<u8>, Vec<u8>>,
72+
#[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::btreemap_as_seq"))]
73+
pub sp_dleq_proofs: BTreeMap<CompressedPublicKey, DleqProof>,
7074

7175
/// Global proprietary key-value pairs.
7276
#[cfg_attr(feature = "serde", serde(with = "crate::serde_utils::btreemap_as_seq_byte_values"))]
@@ -148,9 +152,10 @@ impl Global {
148152
let mut output_count: Option<u64> = None;
149153
let mut xpubs: BTreeMap<Xpub, (Fingerprint, DerivationPath)> = Default::default();
150154
#[cfg(feature = "silent-payments")]
151-
let mut sp_ecdh_shares: BTreeMap<Vec<u8>, Vec<u8>> = Default::default();
155+
let mut sp_ecdh_shares: BTreeMap<CompressedPublicKey, CompressedPublicKey> =
156+
Default::default();
152157
#[cfg(feature = "silent-payments")]
153-
let mut sp_dleq_proofs: BTreeMap<Vec<u8>, Vec<u8>> = Default::default();
158+
let mut sp_dleq_proofs: BTreeMap<CompressedPublicKey, DleqProof> = Default::default();
154159
let mut proprietaries: BTreeMap<raw::ProprietaryKey, Vec<u8>> = Default::default();
155160
let mut unknowns: BTreeMap<raw::Key, Vec<u8>> = Default::default();
156161

@@ -310,41 +315,16 @@ impl Global {
310315
},
311316
#[cfg(feature = "silent-payments")]
312317
PSBT_GLOBAL_SP_ECDH_SHARE => {
313-
if pair.key.key.is_empty() {
314-
return Err(InsertPairError::InvalidKeyDataEmpty(pair.key));
315-
}
316-
if pair.key.key.len() != 33 {
317-
return Err(InsertPairError::KeyWrongLength(pair.key.key.len(), 33));
318-
}
319-
if pair.value.len() != 33 {
320-
return Err(InsertPairError::ValueWrongLength(pair.value.len(), 33));
321-
}
322-
match sp_ecdh_shares.entry(pair.key.key.clone()) {
323-
btree_map::Entry::Vacant(empty_key) => {
324-
empty_key.insert(pair.value);
325-
}
326-
btree_map::Entry::Occupied(_) =>
327-
return Err(InsertPairError::DuplicateKey(pair.key)),
328-
}
318+
v2_impl_psbt_insert_sp_pair!(
319+
sp_ecdh_shares,
320+
pair.key,
321+
pair.value,
322+
compressed_pubkey
323+
);
329324
}
330325
#[cfg(feature = "silent-payments")]
331326
PSBT_GLOBAL_SP_DLEQ => {
332-
if pair.key.key.is_empty() {
333-
return Err(InsertPairError::InvalidKeyDataEmpty(pair.key));
334-
}
335-
if pair.key.key.len() != 33 {
336-
return Err(InsertPairError::KeyWrongLength(pair.key.key.len(), 33));
337-
}
338-
if pair.value.len() != 64 {
339-
return Err(InsertPairError::ValueWrongLength(pair.value.len(), 64));
340-
}
341-
match sp_dleq_proofs.entry(pair.key.key.clone()) {
342-
btree_map::Entry::Vacant(empty_key) => {
343-
empty_key.insert(pair.value);
344-
}
345-
btree_map::Entry::Occupied(_) =>
346-
return Err(InsertPairError::DuplicateKey(pair.key)),
347-
}
327+
v2_impl_psbt_insert_sp_pair!(sp_dleq_proofs, pair.key, pair.value, dleq_proof);
348328
}
349329
v if v == PSBT_GLOBAL_UNSIGNED_TX =>
350330
return Err(InsertPairError::ExcludedKey { key_type_value: v }),
@@ -527,16 +507,22 @@ impl Map for Global {
527507
#[cfg(feature = "silent-payments")]
528508
for (scan_key, ecdh_share) in &self.sp_ecdh_shares {
529509
rv.push(raw::Pair {
530-
key: raw::Key { type_value: PSBT_GLOBAL_SP_ECDH_SHARE, key: scan_key.clone() },
531-
value: ecdh_share.clone(),
510+
key: raw::Key {
511+
type_value: PSBT_GLOBAL_SP_ECDH_SHARE,
512+
key: scan_key.to_bytes().to_vec(),
513+
},
514+
value: ecdh_share.to_bytes().to_vec(),
532515
});
533516
}
534517

535518
#[cfg(feature = "silent-payments")]
536519
for (scan_key, dleq_proof) in &self.sp_dleq_proofs {
537520
rv.push(raw::Pair {
538-
key: raw::Key { type_value: PSBT_GLOBAL_SP_DLEQ, key: scan_key.clone() },
539-
value: dleq_proof.clone(),
521+
key: raw::Key {
522+
type_value: PSBT_GLOBAL_SP_DLEQ,
523+
key: scan_key.to_bytes().to_vec(),
524+
},
525+
value: dleq_proof.as_bytes().to_vec(),
540526
});
541527
}
542528

0 commit comments

Comments
 (0)