Skip to content

Commit 46a877e

Browse files
committed
Merge #45: Add BIP375 fields
57b8fc3 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 (macgyver13) 9177caa Add bip375 specific tests (macgyver13) 59c80b8 Add bip375 fields to consts for global, input, outputs Perform basic decode and field matching based on bip375 spec (macgyver13) e1938eb Add silent-payments feature flag to resolve decode failure Ignore bip370 invalid test PSBT_OUT_SCRIPT (macgyver13) Pull request description: This PR adds global, input, output fields to support bip375 serialization/deserialization. Most additions are guarded by a bip375 feature flag, specifically the two breaking changes in the first commit. This implementation is being tested by [BIP375 rust examples](https://github.com/macgyver13/bip375-examples/tree/main/rust) All feedback welcome. If there is more I can / should do please let me know. ACKs for top commit: nyonson: ACK 57b8fc3 tcharding: ACK 57b8fc3 Tree-SHA512: e9c968a978bffdfc88e9e7808efc618a13cede230939104d76a33e031d7c669499350e0ae8fa19f75ec42a68fb03a5c0c4839ac1213f29f04897572d3f3d73a4
2 parents 0615379 + 57b8fc3 commit 46a877e

File tree

12 files changed

+843
-42
lines changed

12 files changed

+843
-42
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ rand = ["std", "bitcoin/rand-std"]
2323
serde = ["dep:serde", "bitcoin/serde"]
2424
base64 = ["bitcoin/base64"]
2525
miniscript = ["dep:miniscript", "miniscript?/no-std"]
26+
silent-payments = [] # Silent Payments support
2627

2728
[dependencies]
2829
bitcoin = { version = "0.32.8", default-features = false }

src/consts.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ pub(crate) const PSBT_GLOBAL_INPUT_COUNT: u8 = 0x04;
2121
pub(crate) const PSBT_GLOBAL_OUTPUT_COUNT: u8 = 0x05;
2222
/// Type: Transaction Modifiable Flags PSBT_GLOBAL_TX_MODIFIABLE = 0x06
2323
pub(crate) const PSBT_GLOBAL_TX_MODIFIABLE: u8 = 0x06;
24+
#[cfg(feature = "silent-payments")]
25+
/// Type: Silent Payment ECDH Share PSBT_GLOBAL_SP_ECDH_SHARE = 0x07
26+
pub(crate) const PSBT_GLOBAL_SP_ECDH_SHARE: u8 = 0x07;
27+
#[cfg(feature = "silent-payments")]
28+
/// Type: Silent Payment DLEQ PSBT_GLOBAL_SP_DLEQ = 0x08
29+
pub(crate) const PSBT_GLOBAL_SP_DLEQ: u8 = 0x08;
2430
/// Type: Version Number PSBT_GLOBAL_VERSION = 0xFB
2531
pub(crate) const PSBT_GLOBAL_VERSION: u8 = 0xFB;
2632
/// Type: Proprietary Use Type PSBT_GLOBAL_PROPRIETARY = 0xFC
@@ -77,6 +83,12 @@ pub(crate) const PSBT_IN_TAP_BIP32_DERIVATION: u8 = 0x16;
7783
pub(crate) const PSBT_IN_TAP_INTERNAL_KEY: u8 = 0x17;
7884
/// Type: Taproot Merkle Root PSBT_IN_TAP_MERKLE_ROOT = 0x18
7985
pub(crate) const PSBT_IN_TAP_MERKLE_ROOT: u8 = 0x18;
86+
#[cfg(feature = "silent-payments")]
87+
/// Type: Silent Payment ECDH Share PSBT_IN_SP_ECDH_SHARE = 0x1D
88+
pub(crate) const PSBT_IN_SP_ECDH_SHARE: u8 = 0x1D;
89+
#[cfg(feature = "silent-payments")]
90+
/// Type: Silent Payment DLEQ Proof PSBT_IN_SP_DLEQ = 0x1E
91+
pub(crate) const PSBT_IN_SP_DLEQ: u8 = 0x1E;
8092
/// Type: Proprietary Use Type PSBT_IN_PROPRIETARY = 0xFC
8193
pub(crate) const PSBT_IN_PROPRIETARY: u8 = 0xFC;
8294

@@ -96,6 +108,12 @@ pub(crate) const PSBT_OUT_TAP_INTERNAL_KEY: u8 = 0x05;
96108
pub(crate) const PSBT_OUT_TAP_TREE: u8 = 0x06;
97109
/// Type: Taproot Key BIP 32 Derivation Path PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
98110
pub(crate) const PSBT_OUT_TAP_BIP32_DERIVATION: u8 = 0x07;
111+
#[cfg(feature = "silent-payments")]
112+
/// Type: Silent Payment v0 Info PSBT_OUT_SP_V0_INFO = 0x09
113+
pub(crate) const PSBT_OUT_SP_V0_INFO: u8 = 0x09;
114+
#[cfg(feature = "silent-payments")]
115+
/// Type: Silent Payment v0 Label PSBT_OUT_SP_V0_LABEL = 0x0A
116+
pub(crate) const PSBT_OUT_SP_V0_LABEL: u8 = 0x0A;
99117
/// Type: Proprietary Use Type PSBT_IN_PROPRIETARY = 0xFC
100118
pub(crate) const PSBT_OUT_PROPRIETARY: u8 = 0xFC;
101119

@@ -109,6 +127,10 @@ pub(crate) fn psbt_global_key_type_value_to_str(v: u8) -> &'static str {
109127
PSBT_GLOBAL_INPUT_COUNT => "PSBT_GLOBAL_INPUT_COUNT",
110128
PSBT_GLOBAL_OUTPUT_COUNT => "PSBT_GLOBAL_OUTPUT_COUNT",
111129
PSBT_GLOBAL_TX_MODIFIABLE => "PSBT_GLOBAL_TX_MODIFIABLE",
130+
#[cfg(feature = "silent-payments")]
131+
PSBT_GLOBAL_SP_ECDH_SHARE => "PSBT_GLOBAL_SP_ECDH_SHARE",
132+
#[cfg(feature = "silent-payments")]
133+
PSBT_GLOBAL_SP_DLEQ => "PSBT_GLOBAL_SP_DLEQ",
112134
PSBT_GLOBAL_VERSION => "PSBT_GLOBAL_VERSION",
113135
PSBT_GLOBAL_PROPRIETARY => "PSBT_GLOBAL_PROPRIETARY",
114136
_ => "unknown PSBT_GLOBAL_ key type value",
@@ -143,6 +165,10 @@ pub(crate) fn psbt_in_key_type_value_to_str(v: u8) -> &'static str {
143165
PSBT_IN_TAP_BIP32_DERIVATION => "PSBT_IN_TAP_BIP32_DERIVATION",
144166
PSBT_IN_TAP_INTERNAL_KEY => "PSBT_IN_TAP_INTERNAL_KEY",
145167
PSBT_IN_TAP_MERKLE_ROOT => "PSBT_IN_TAP_MERKLE_ROOT",
168+
#[cfg(feature = "silent-payments")]
169+
PSBT_IN_SP_ECDH_SHARE => "PSBT_IN_SP_ECDH_SHARE",
170+
#[cfg(feature = "silent-payments")]
171+
PSBT_IN_SP_DLEQ => "PSBT_IN_SP_DLEQ",
146172
PSBT_IN_PROPRIETARY => "PSBT_IN_PROPRIETARY",
147173
_ => "unknown PSBT_IN_ key type value",
148174
}
@@ -159,6 +185,10 @@ pub(crate) fn psbt_out_key_type_value_to_str(v: u8) -> &'static str {
159185
PSBT_OUT_TAP_INTERNAL_KEY => "PSBT_OUT_TAP_INTERNAL_KEY",
160186
PSBT_OUT_TAP_TREE => "PSBT_OUT_TAP_TREE",
161187
PSBT_OUT_TAP_BIP32_DERIVATION => "PSBT_OUT_TAP_BIP32_DERIVATION",
188+
#[cfg(feature = "silent-payments")]
189+
PSBT_OUT_SP_V0_INFO => "PSBT_OUT_SP_V0_INFO",
190+
#[cfg(feature = "silent-payments")]
191+
PSBT_OUT_SP_V0_LABEL => "PSBT_OUT_SP_V0_LABEL",
162192
PSBT_OUT_PROPRIETARY => "PSBT_OUT_PROPRIETARY",
163193
_ => "unknown PSBT_OUT_ key type value",
164194
}

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 actual_serde::de::Visitor<'_> 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 actual_serde::de::Visitor<'_> 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 {}

0 commit comments

Comments
 (0)