Skip to content

Commit 3c958e2

Browse files
committed
blind: encapsulate RangeProofMessage and improve error types
In #279 we fixed some slicing bugs in a fairly ugly way so that we could easily backport the bugfixes without API breakage. However, the better solution is to close the RangeProofMessage type, add a dedicated error for parsing from an array, and update all callers. This changes the CT validation logic to allow rangeproofs to contain embedded asset IDs alongside explicit assets. This differs from Elements which only allows unblinding an output if both its value and asset commitment are confidential. However, if the confidential asset is explicit, it seems like the "right thing to do" to embed the asset ID and a zero blinding factor. This matches what we do in this library in TxIn::blind_issuances_with_bfs for example.
1 parent ff1abab commit 3c958e2

3 files changed

Lines changed: 163 additions & 66 deletions

File tree

clippy.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
avoid-breaking-exported-api = true
2+
large-error-threshold = 192 # default 128 complains about two public keys in RangeProofMessageError

src/blind.rs

Lines changed: 161 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
//! # Transactions Blinding
1616
//!
1717
18-
use internals::array::ArrayExt as _;
1918
use internals::slice::SliceExt;
2019
use std::{self, collections::BTreeMap, fmt};
2120

@@ -201,26 +200,144 @@ impl From<secp256k1_zkp::Error> for ConfidentialTxOutError {
201200
ConfidentialTxOutError::Upstream(from)
202201
}
203202
}
204-
/// The Rangeproof message
205-
#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
206-
pub struct RangeProofMessage {
207-
/// The asset id
208-
pub asset: AssetId,
209-
/// The asset blinding factor
210-
pub bf: AssetBlindingFactor,
211-
}
212203

213-
impl RangeProofMessage {
214-
/// Converts the message to bytes
215-
pub fn to_bytes(&self) -> [u8; 64] {
216-
let mut message = [0u8; 64];
204+
mod range_proof_message {
205+
use core::fmt;
206+
use internals::array::ArrayExt;
207+
use secp256k1_zkp::{Generator, Signing, Secp256k1};
208+
use super::{Asset, AssetId, AssetBlindingFactor};
209+
210+
/// The Rangeproof message
211+
#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
212+
pub struct RangeProofMessage {
213+
/// The asset id
214+
asset_id: AssetId,
215+
/// The asset blinding factor
216+
asset_bf: AssetBlindingFactor,
217+
}
218+
219+
impl RangeProofMessage {
220+
/// Constructs a [`RangeProofMessage`] from an asset ID and blinding factor.
221+
pub fn new(asset_id: AssetId, asset_bf: AssetBlindingFactor) -> Self {
222+
Self { asset_id, asset_bf }
223+
}
224+
225+
/// The asset ID embedded in the rangeproof message.
226+
pub fn asset_id(&self) -> &AssetId {
227+
&self.asset_id
228+
}
229+
230+
/// The asset blinding factor embedded in the rangeproof message.
231+
pub fn blinding_factor(&self) -> &AssetBlindingFactor {
232+
&self.asset_bf
233+
}
217234

218-
message[..32].copy_from_slice(self.asset.into_tag().as_ref());
219-
message[32..].copy_from_slice(self.bf.into_inner().as_ref());
235+
/// Computes the commmitment of the rangeproof message.
236+
pub fn commitment<C: Signing>(&self, secp: &Secp256k1<C>) -> Generator {
237+
Generator::new_blinded(secp, self.asset_id.into_tag(), self.asset_bf.into_inner())
238+
}
220239

221-
message
240+
/// Parses a message from bytes
241+
pub fn from_byte_array<C: Signing>(
242+
secp: &Secp256k1<C>,
243+
inner: [u8; 64],
244+
expected_asset: &Asset,
245+
) -> Result<Self, RangeProofMessageError> {
246+
let (asset_id, asset_bf) = inner.split_array::<32, 32>();
247+
let ret = Self {
248+
asset_id: AssetId::from_byte_array(*asset_id),
249+
asset_bf: AssetBlindingFactor::from_byte_array(*asset_bf)
250+
.map_err(RangeProofMessageError::BlindingFactorOutOfRange)?,
251+
};
252+
253+
match expected_asset {
254+
Asset::Null => return Err(RangeProofMessageError::NullExpectedAsset),
255+
Asset::Explicit(asset_id) => {
256+
if ret.asset_id != *asset_id {
257+
return Err(RangeProofMessageError::ExplicitAssetMismatch {
258+
in_txout: *asset_id,
259+
in_message: ret.asset_id,
260+
})
261+
}
262+
if ret.asset_bf != AssetBlindingFactor::zero() {
263+
return Err(RangeProofMessageError::ExplicitAssetNonzeroBf {
264+
blinding_factor: ret.asset_bf,
265+
});
266+
}
267+
}
268+
Asset::Confidential(commitment) => {
269+
let ret_commitment = ret.commitment(secp);
270+
if ret_commitment != *commitment {
271+
return Err(RangeProofMessageError::ConfidentialAssetMismatch {
272+
in_txout: *commitment,
273+
in_message: ret_commitment,
274+
})
275+
}
276+
}
277+
}
278+
279+
Ok(ret)
280+
}
281+
282+
/// Converts the message to bytes
283+
pub fn to_byte_array(&self) -> [u8; 64] {
284+
let mut message = [0u8; 64];
285+
message[..32].copy_from_slice(self.asset_id.into_tag().as_ref());
286+
message[32..].copy_from_slice(self.asset_bf.into_inner().as_ref());
287+
message
288+
}
289+
}
290+
291+
#[non_exhaustive]
292+
#[derive(PartialEq, Eq, Clone, Debug)]
293+
pub enum RangeProofMessageError {
294+
NullExpectedAsset,
295+
ExplicitAssetMismatch {
296+
in_txout: AssetId,
297+
in_message: AssetId,
298+
},
299+
ExplicitAssetNonzeroBf {
300+
blinding_factor: AssetBlindingFactor,
301+
},
302+
ConfidentialAssetMismatch {
303+
in_txout: Generator,
304+
in_message: Generator,
305+
},
306+
BlindingFactorOutOfRange(secp256k1_zkp::Error),
307+
}
308+
309+
impl fmt::Display for RangeProofMessageError {
310+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311+
match *self {
312+
Self::NullExpectedAsset => f.write_str("rangeproof associated with null asset"),
313+
Self::ExplicitAssetMismatch { in_txout, in_message } => {
314+
write!(f, "txout had explicit asset ID {in_txout}, but rangeproof encoded asset ID {in_message}")
315+
}
316+
Self::ExplicitAssetNonzeroBf { blinding_factor } => {
317+
write!(f, "txout had explicit asset ID, but rangeproof encoded a nonzero asset blinding factor {blinding_factor}")
318+
}
319+
Self::ConfidentialAssetMismatch { in_txout, in_message } => {
320+
write!(f, "txout had asset commitment {in_txout}, but rangeproof encoded asset commitment {in_message}")
321+
}
322+
Self::BlindingFactorOutOfRange(ref e) => e.fmt(f),
323+
}
324+
}
325+
}
326+
327+
impl std::error::Error for RangeProofMessageError {
328+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
329+
match *self {
330+
Self::NullExpectedAsset => None,
331+
Self::ExplicitAssetMismatch { .. } => None,
332+
Self::ExplicitAssetNonzeroBf { .. } => None,
333+
Self::ConfidentialAssetMismatch { .. } => None,
334+
Self::BlindingFactorOutOfRange(ref e) => Some(e),
335+
}
336+
337+
}
222338
}
223339
}
340+
pub use self::range_proof_message::{RangeProofMessage, RangeProofMessageError};
224341

225342
/// Information about Transaction Input Asset
226343
#[cfg_attr(
@@ -432,8 +549,7 @@ impl Value {
432549
let value = self
433550
.explicit()
434551
.ok_or(ConfidentialTxOutError::ExpectedExplicitValue)?;
435-
let out_asset_commitment =
436-
Generator::new_blinded(secp, msg.asset.into_tag(), msg.bf.into_inner());
552+
let out_asset_commitment = msg.commitment(secp);
437553
let value_commitment = Value::new_confidential(secp, value, out_asset_commitment, vbf);
438554

439555
let rangeproof = RangeProof::new(
@@ -442,7 +558,7 @@ impl Value {
442558
value_commitment.commitment().expect("confidential value"),
443559
value,
444560
vbf.into_inner(),
445-
&msg.to_bytes(),
561+
&msg.to_byte_array(),
446562
spk.as_bytes(),
447563
shared_secret,
448564
TxOut::RANGEPROOF_EXP_SHIFT,
@@ -536,10 +652,10 @@ impl TxOut {
536652
let (out_asset, surjection_proof) =
537653
exp_asset.blind(rng, secp, out_secrets.asset_bf, spent_utxo_secrets)?;
538654

539-
let msg = RangeProofMessage {
540-
asset: out_secrets.asset,
541-
bf: out_secrets.asset_bf,
542-
};
655+
let msg = RangeProofMessage::new(
656+
out_secrets.asset,
657+
out_secrets.asset_bf,
658+
);
543659
let exp_value = Value::Explicit(out_secrets.value);
544660
let (out_value, nonce, range_proof) = exp_value.blind(
545661
secp,
@@ -735,7 +851,7 @@ impl TxOut {
735851
/// Unblinds a transaction output, if it is confidential.
736852
///
737853
/// It returns the secret elements of the value and asset Pedersen commitments.
738-
pub fn unblind<C: Verification>(
854+
pub fn unblind<C: Signing + Verification>(
739855
&self,
740856
secp: &Secp256k1<C>,
741857
blinding_key: SecretKey,
@@ -760,34 +876,22 @@ impl TxOut {
760876
shared_secret,
761877
self.script_pubkey.as_bytes(),
762878
additional_generator,
763-
)?;
879+
).map_err(UnblindError::Rewind)?;
764880

765-
// Use `MissingRangeproof` error because it's available so does not require
766-
// API breaks. In a later PR we should extend that enum and add #[non_exhaustive]
767-
// to it. The maybe-better `MalformedAssetId` error requires we start with a
768-
// std `FromSliceError` which we don't have.
881+
let value = opening.value;
882+
let value_bf = ValueBlindingFactor(opening.blinding_factor);
769883
let asset_and_bf = SliceExt::split_first_chunk::<64>(opening.message.as_ref())
770884
.ok_or(UnblindError::MissingRangeproof)?
771885
.0;
772-
let (asset_id, asset_bf) = asset_and_bf.split_array();
773-
774-
let asset_id = AssetId::from_byte_array(*asset_id);
775-
let asset_bf = AssetBlindingFactor::from_byte_array(*asset_bf)?;
776-
if let Asset::Confidential(own_asset) = self.asset {
777-
let secp = Secp256k1::signing_only(); // needed to avoid API break
778-
let asset = Generator::new_blinded(&secp, asset_id.into_tag(), asset_bf.into_inner());
779-
if asset != own_asset {
780-
// See above about use of MissingRangeproof.
781-
return Err(UnblindError::MissingRangeproof);
782-
}
783-
}
784-
785-
let value = opening.value;
786-
let value_bf = ValueBlindingFactor(opening.blinding_factor);
886+
let message = RangeProofMessage::from_byte_array(
887+
secp,
888+
*asset_and_bf,
889+
&self.asset,
890+
).map_err(UnblindError::RangeProofMessage)?;
787891

788892
Ok(TxOutSecrets {
789-
asset: asset_id,
790-
asset_bf,
893+
asset: *message.asset_id(),
894+
asset_bf: *message.blinding_factor(),
791895
value,
792896
value_bf,
793897
})
@@ -796,25 +900,26 @@ impl TxOut {
796900

797901
/// Errors encountered when unblinding `TxOut`s.
798902
#[derive(Debug)]
903+
#[non_exhaustive]
799904
pub enum UnblindError {
800905
/// The `TxOut` is not fully confidential.
801906
NotConfidential,
802907
/// Transaction output does not have a nonce commitment.
803908
MissingNonce,
804909
/// Transaction output does not have a rangeproof.
805910
MissingRangeproof,
806-
/// Malformed asset ID.
807-
MalformedAssetId(core::array::TryFromSliceError),
911+
/// Malformed rangeproof message.
912+
RangeProofMessage(RangeProofMessageError),
808913
/// Error originated in `secp256k1_zkp`.
809-
Upstream(secp256k1_zkp::Error),
914+
Rewind(secp256k1_zkp::Error),
810915
}
811916

812917
impl fmt::Display for UnblindError {
813918
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
814919
match self {
815920
UnblindError::MissingNonce => write!(f, "missing nonce in txout"),
816-
UnblindError::MalformedAssetId(_) => write!(f, "malformed asset id"),
817-
UnblindError::Upstream(e) => write!(f, "{}", e),
921+
UnblindError::RangeProofMessage(_) => f.write_str("failed to parse message embedded in rangeproof"),
922+
UnblindError::Rewind(_) => f.write_str("failed to rewind rangeproof"),
818923
UnblindError::NotConfidential => write!(f, "cannot unblind non-confidential txout"),
819924
UnblindError::MissingRangeproof => write!(f, "missing rangeproof in txout"),
820925
}
@@ -825,20 +930,14 @@ impl std::error::Error for UnblindError {
825930
fn cause(&self) -> Option<&(dyn std::error::Error + 'static)> {
826931
match self {
827932
UnblindError::MissingNonce => None,
828-
UnblindError::MalformedAssetId(e) => Some(e),
829-
UnblindError::Upstream(e) => Some(e),
933+
UnblindError::RangeProofMessage(e) => Some(e),
934+
UnblindError::Rewind(e) => Some(e),
830935
UnblindError::NotConfidential => None,
831936
UnblindError::MissingRangeproof => None,
832937
}
833938
}
834939
}
835940

836-
impl From<secp256k1_zkp::Error> for UnblindError {
837-
fn from(from: secp256k1_zkp::Error) -> Self {
838-
UnblindError::Upstream(from)
839-
}
840-
}
841-
842941
impl TxIn {
843942
/// Blind issuances for this [`TxIn`]. Asset amount and token amount must be
844943
/// set in [`AssetIssuance`](crate::AssetIssuance) field for this input
@@ -871,10 +970,10 @@ impl TxIn {
871970
Value::Explicit(v) => Value::Explicit(v),
872971
};
873972
let spk = Script::new();
874-
let msg = RangeProofMessage {
973+
let msg = RangeProofMessage::new(
875974
asset,
876-
bf: AssetBlindingFactor::zero(),
877-
};
975+
AssetBlindingFactor::zero(),
976+
);
878977
let (comm, prf) = v.blind_with_shared_secret(secp, bf, blind_sk, &spk, &msg)?;
879978
if i == 0 {
880979
self.asset_issuance.amount = comm;

src/pset/mod.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -654,10 +654,7 @@ impl PartiallySignedTransaction {
654654
.ok_or(PsetBlindError::MustHaveExplicitTxOut(last_out_index))?;
655655
let ephemeral_sk = SecretKey::new(rng);
656656
let spk = &self.outputs[last_out_index].script_pubkey;
657-
let msg = RangeProofMessage {
658-
asset: asset_id,
659-
bf: out_abf,
660-
};
657+
let msg = RangeProofMessage::new(asset_id, out_abf);
661658
let blind_res = exp_value.blind(
662659
secp,
663660
final_vbf,

0 commit comments

Comments
 (0)