Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ rand = ["std", "bitcoin/rand-std"]
serde = ["dep:actual-serde", "bitcoin/serde"]
base64 = ["bitcoin/base64"]
miniscript = ["dep:miniscript", "miniscript?/no-std"]
silent-payments = [] # Silent Payments support

[dependencies]
bitcoin = { version = "0.32.6", default-features = false }
Expand Down
30 changes: 30 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ pub(crate) const PSBT_GLOBAL_INPUT_COUNT: u8 = 0x04;
pub(crate) const PSBT_GLOBAL_OUTPUT_COUNT: u8 = 0x05;
/// Type: Transaction Modifiable Flags PSBT_GLOBAL_TX_MODIFIABLE = 0x06
pub(crate) const PSBT_GLOBAL_TX_MODIFIABLE: u8 = 0x06;
#[cfg(feature = "silent-payments")]
/// Type: Silent Payment ECDH Share PSBT_GLOBAL_SP_ECDH_SHARE = 0x07
pub(crate) const PSBT_GLOBAL_SP_ECDH_SHARE: u8 = 0x07;
#[cfg(feature = "silent-payments")]
/// Type: Silent Payment DLEQ PSBT_GLOBAL_SP_DLEQ = 0x08
pub(crate) const PSBT_GLOBAL_SP_DLEQ: u8 = 0x08;
/// Type: Version Number PSBT_GLOBAL_VERSION = 0xFB
pub(crate) const PSBT_GLOBAL_VERSION: u8 = 0xFB;
/// Type: Proprietary Use Type PSBT_GLOBAL_PROPRIETARY = 0xFC
Expand Down Expand Up @@ -77,6 +83,12 @@ pub(crate) const PSBT_IN_TAP_BIP32_DERIVATION: u8 = 0x16;
pub(crate) const PSBT_IN_TAP_INTERNAL_KEY: u8 = 0x17;
/// Type: Taproot Merkle Root PSBT_IN_TAP_MERKLE_ROOT = 0x18
pub(crate) const PSBT_IN_TAP_MERKLE_ROOT: u8 = 0x18;
#[cfg(feature = "silent-payments")]
/// Type: Silent Payment ECDH Share PSBT_IN_SP_ECDH_SHARE = 0x1D
pub(crate) const PSBT_IN_SP_ECDH_SHARE: u8 = 0x1D;
#[cfg(feature = "silent-payments")]
/// Type: Silent Payment DLEQ Proof PSBT_IN_SP_DLEQ = 0x1E
pub(crate) const PSBT_IN_SP_DLEQ: u8 = 0x1E;
/// Type: Proprietary Use Type PSBT_IN_PROPRIETARY = 0xFC
pub(crate) const PSBT_IN_PROPRIETARY: u8 = 0xFC;

Expand All @@ -96,6 +108,12 @@ pub(crate) const PSBT_OUT_TAP_INTERNAL_KEY: u8 = 0x05;
pub(crate) const PSBT_OUT_TAP_TREE: u8 = 0x06;
/// Type: Taproot Key BIP 32 Derivation Path PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
pub(crate) const PSBT_OUT_TAP_BIP32_DERIVATION: u8 = 0x07;
#[cfg(feature = "silent-payments")]
/// Type: Silent Payment v0 Info PSBT_OUT_SP_V0_INFO = 0x09
pub(crate) const PSBT_OUT_SP_V0_INFO: u8 = 0x09;
#[cfg(feature = "silent-payments")]
/// Type: Silent Payment v0 Label PSBT_OUT_SP_V0_LABEL = 0x0A
pub(crate) const PSBT_OUT_SP_V0_LABEL: u8 = 0x0A;
/// Type: Proprietary Use Type PSBT_IN_PROPRIETARY = 0xFC
pub(crate) const PSBT_OUT_PROPRIETARY: u8 = 0xFC;

Expand All @@ -109,6 +127,10 @@ pub(crate) fn psbt_global_key_type_value_to_str(v: u8) -> &'static str {
PSBT_GLOBAL_INPUT_COUNT => "PSBT_GLOBAL_INPUT_COUNT",
PSBT_GLOBAL_OUTPUT_COUNT => "PSBT_GLOBAL_OUTPUT_COUNT",
PSBT_GLOBAL_TX_MODIFIABLE => "PSBT_GLOBAL_TX_MODIFIABLE",
#[cfg(feature = "silent-payments")]
PSBT_GLOBAL_SP_ECDH_SHARE => "PSBT_GLOBAL_SP_ECDH_SHARE",
#[cfg(feature = "silent-payments")]
PSBT_GLOBAL_SP_DLEQ => "PSBT_GLOBAL_SP_DLEQ",
PSBT_GLOBAL_VERSION => "PSBT_GLOBAL_VERSION",
PSBT_GLOBAL_PROPRIETARY => "PSBT_GLOBAL_PROPRIETARY",
_ => "unknown PSBT_GLOBAL_ key type value",
Expand Down Expand Up @@ -143,6 +165,10 @@ pub(crate) fn psbt_in_key_type_value_to_str(v: u8) -> &'static str {
PSBT_IN_TAP_BIP32_DERIVATION => "PSBT_IN_TAP_BIP32_DERIVATION",
PSBT_IN_TAP_INTERNAL_KEY => "PSBT_IN_TAP_INTERNAL_KEY",
PSBT_IN_TAP_MERKLE_ROOT => "PSBT_IN_TAP_MERKLE_ROOT",
#[cfg(feature = "silent-payments")]
PSBT_IN_SP_ECDH_SHARE => "PSBT_IN_SP_ECDH_SHARE",
#[cfg(feature = "silent-payments")]
PSBT_IN_SP_DLEQ => "PSBT_IN_SP_DLEQ",
PSBT_IN_PROPRIETARY => "PSBT_IN_PROPRIETARY",
_ => "unknown PSBT_IN_ key type value",
}
Expand All @@ -159,6 +185,10 @@ pub(crate) fn psbt_out_key_type_value_to_str(v: u8) -> &'static str {
PSBT_OUT_TAP_INTERNAL_KEY => "PSBT_OUT_TAP_INTERNAL_KEY",
PSBT_OUT_TAP_TREE => "PSBT_OUT_TAP_TREE",
PSBT_OUT_TAP_BIP32_DERIVATION => "PSBT_OUT_TAP_BIP32_DERIVATION",
#[cfg(feature = "silent-payments")]
PSBT_OUT_SP_V0_INFO => "PSBT_OUT_SP_V0_INFO",
#[cfg(feature = "silent-payments")]
PSBT_OUT_SP_V0_LABEL => "PSBT_OUT_SP_V0_LABEL",
PSBT_OUT_PROPRIETARY => "PSBT_OUT_PROPRIETARY",
_ => "unknown PSBT_OUT_ key type value",
}
Expand Down
39 changes: 39 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,42 @@ macro_rules! v2_impl_psbt_hash_serialize {
}
};
}

/// Macro for inserting BIP-375 silent payment fields with CompressedPublicKey keys.
#[cfg(feature = "silent-payments")]
macro_rules! v2_impl_psbt_insert_sp_pair {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I prefer the locality and duplicity of the code rather than the neat but more complex macro abstraction. I recommend to remove and replace this macro by the actual code in the decode method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered the theme of v2_impl_psbt_insert_pair when making the decision.
Would having separate macros insert_sp_ecdh and insert_sp_dleq be a reasonable middle ground?

I am happy to alter this if inline is the preference of the maintainers, just let me know.

Thank you for the review!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't forgotten you mate, just a bit snowed under atm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition is not urgent, take your time!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely have a bias against macros, but in this case, it does look like it's following the existing patterns of the crate. That makes it easier to digest and possibly refactor in the future if we clean up the macros. But I'm a new contributor here, so will defer to @tcharding 's take on it.

// For CompressedPublicKey values (ECDH shares)
($map:expr, $raw_key:expr, $raw_value:expr, compressed_pubkey) => {{
if $raw_key.key.is_empty() {
return Err(InsertPairError::InvalidKeyDataEmpty($raw_key).into());
}
let scan_key = bitcoin::CompressedPublicKey::from_slice(&$raw_key.key)
.map_err(|_| InsertPairError::KeyWrongLength($raw_key.key.len(), 33))?;
let value = bitcoin::CompressedPublicKey::from_slice(&$raw_value)
.map_err(|_| InsertPairError::ValueWrongLength($raw_value.len(), 33))?;
match $map.entry(scan_key) {
$crate::prelude::btree_map::Entry::Vacant(empty_key) => {
empty_key.insert(value);
}
$crate::prelude::btree_map::Entry::Occupied(_) =>
return Err(InsertPairError::DuplicateKey($raw_key).into()),
}
}};
// For DleqProof values (DLEQ proofs)
($map:expr, $raw_key:expr, $raw_value:expr, dleq_proof) => {{
if $raw_key.key.is_empty() {
return Err(InsertPairError::InvalidKeyDataEmpty($raw_key).into());
}
let scan_key = bitcoin::CompressedPublicKey::from_slice(&$raw_key.key)
.map_err(|_| InsertPairError::KeyWrongLength($raw_key.key.len(), 33))?;
let value = $crate::v2::dleq::DleqProof::try_from($raw_value.as_slice())
.map_err(|_| InsertPairError::ValueWrongLength($raw_value.len(), 64))?;
match $map.entry(scan_key) {
$crate::prelude::btree_map::Entry::Vacant(empty_key) => {
empty_key.insert(value);
}
$crate::prelude::btree_map::Entry::Occupied(_) =>
return Err(InsertPairError::DuplicateKey($raw_key).into()),
}
}};
}
35 changes: 34 additions & 1 deletion src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,27 @@ pub enum Error {
LockTime(absolute::ConversionError),
/// Unsupported PSBT version.
UnsupportedVersion(version::UnsupportedVersionError),
/// Invalid scan key for BIP-375 silent payments (expected 33 bytes).
InvalidScanKey {
/// The length that was provided.
got: usize,
/// The expected length.
expected: usize,
},
/// Invalid ECDH share for BIP-375 silent payments (expected 33 bytes).
InvalidEcdhShare {
/// The length that was provided.
got: usize,
/// The expected length.
expected: usize,
},
/// Invalid DLEQ proof for BIP-375 silent payments (expected 64 bytes).
InvalidDleqProof {
/// The length that was provided.
got: usize,
/// The expected length.
expected: usize,
},
Comment on lines +431 to +436
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify the error handling by using nested errors, eg

    /// Invalid DLEQ proof for BIP-375 silent payments (expected 64 bytes).
    InvalidDleqProof(dleq::InvalidLengthError),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dleq.rs is conditionally imported with feature flag, are you good with this in serialize.rs::Error?

#[cfg(feature = "silent-payments")]
use crate::v2::dleq;

...

/// Invalid DLEQ proof for BIP-375 silent payments (expected 64 bytes).
#[cfg(feature = "silent-payments")]
InvalidDleqProof(dleq::InvalidLengthError),

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a 2 second look, it looks fine to me mate.

}

impl fmt::Display for Error {
Expand Down Expand Up @@ -440,6 +461,15 @@ impl fmt::Display for Error {
f.write_str("data not consumed entirely when explicitly deserializing"),
LockTime(ref e) => write_err!(f, "parsed locktime invalid"; e),
UnsupportedVersion(ref e) => write_err!(f, "unsupported version"; e),
InvalidScanKey { got, expected } => {
write!(f, "invalid scan key: got {} bytes, expected {}", got, expected)
}
InvalidEcdhShare { got, expected } => {
write!(f, "invalid ECDH share: got {} bytes, expected {}", got, expected)
}
InvalidDleqProof { got, expected } => {
write!(f, "invalid DLEQ proof: got {} bytes, expected {}", got, expected)
}
}
}
}
Expand Down Expand Up @@ -467,7 +497,10 @@ impl std::error::Error for Error {
| InvalidLeafVersion
| Taproot(_)
| TapTree(_)
| PartialDataConsumption => None,
| PartialDataConsumption
| InvalidScanKey { .. }
| InvalidEcdhShare { .. }
| InvalidDleqProof { .. } => None,
}
}
}
Expand Down
141 changes: 141 additions & 0 deletions src/v2/dleq.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: CC0-1.0

//! BIP-375: Support for silent payments in PSBTs.
//!
//! This module provides type-safe wrapper for BIP-374 dleq proof field.
use core::fmt;

use crate::prelude::*;
use crate::serialize::{Deserialize, Serialize};

/// A 64-byte DLEQ proof (BIP-374).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DleqProof(pub [u8; 64]);

#[cfg(feature = "serde")]
impl actual_serde::Serialize for DleqProof {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: actual_serde::Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(&bitcoin::hex::DisplayHex::to_lower_hex_string(&self.0[..]))
} else {
serializer.serialize_bytes(&self.0[..])
}
}
}

#[cfg(feature = "serde")]
impl<'de> actual_serde::Deserialize<'de> for DleqProof {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: actual_serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
struct HexVisitor;
impl actual_serde::de::Visitor<'_> for HexVisitor {
type Value = DleqProof;

fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("a 64-byte hex string")
}

fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: actual_serde::de::Error,
{
use bitcoin::hex::FromHex;
let vec = Vec::<u8>::from_hex(s).map_err(E::custom)?;
DleqProof::try_from(vec).map_err(|e| {
E::custom(format!("expected {} bytes, got {}", e.expected, e.got))
})
}
}
deserializer.deserialize_str(HexVisitor)
} else {
struct BytesVisitor;
impl actual_serde::de::Visitor<'_> for BytesVisitor {
type Value = DleqProof;

fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("64 bytes")
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: actual_serde::de::Error,
{
DleqProof::try_from(v).map_err(|e| {
E::custom(format!("expected {} bytes, got {}", e.expected, e.got))
})
}
}
deserializer.deserialize_bytes(BytesVisitor)
}
}
}

impl DleqProof {
/// Creates a new [`DleqProof`] from a 64-byte array.
pub fn new(bytes: [u8; 64]) -> Self { DleqProof(bytes) }
Comment on lines +81 to +82
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps drop this? We have From already and also a public inner field.


/// Returns the inner 64-byte array.
pub fn as_bytes(&self) -> &[u8; 64] { &self.0 }
}

impl From<[u8; 64]> for DleqProof {
fn from(bytes: [u8; 64]) -> Self { DleqProof(bytes) }
}

impl AsRef<[u8]> for DleqProof {
fn as_ref(&self) -> &[u8] { &self.0 }
}

impl TryFrom<&[u8]> for DleqProof {
type Error = InvalidLengthError;

fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
<[u8; 64]>::try_from(slice)
.map(DleqProof)
.map_err(|_| InvalidLengthError { got: slice.len(), expected: 64 })
}
}

impl TryFrom<Vec<u8>> for DleqProof {
type Error = InvalidLengthError;

fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> { Self::try_from(v.as_slice()) }
}

impl Serialize for DleqProof {
fn serialize(&self) -> Vec<u8> { self.0.to_vec() }
}

impl Deserialize for DleqProof {
fn deserialize(bytes: &[u8]) -> Result<Self, crate::serialize::Error> {
DleqProof::try_from(bytes).map_err(|e| crate::serialize::Error::InvalidDleqProof {
got: e.got,
expected: e.expected,
})
}
}

/// Error returned when a byte array has an invalid length for a dleq proof.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidLengthError {
/// The length that was provided.
pub got: usize,
/// The expected length.
pub expected: usize,
}

impl fmt::Display for InvalidLengthError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid length for BIP-375 type: got {}, expected {}", self.got, self.expected)
}
}

#[cfg(feature = "std")]
impl std::error::Error for InvalidLengthError {}
Loading