Skip to content

Commit 04b0774

Browse files
committed
Add an ability to add blinding key to IssuanceInputConstraints
1 parent b0ffdf1 commit 04b0774

7 files changed

Lines changed: 86 additions & 64 deletions

File tree

crates/contracts/src/sdk/finance/options/creation_option.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ pub fn build_option_creation(
165165
reissuance_destination: Some((
166166
options_taproot_pubkey_gen.address.script_pubkey(),
167167
1,
168+
None,
168169
)),
169170
},
170171
IssuanceInputConstraints {
@@ -173,6 +174,7 @@ pub fn build_option_creation(
173174
reissuance_destination: Some((
174175
options_taproot_pubkey_gen.address.script_pubkey(),
175176
1,
177+
None,
176178
)),
177179
},
178180
],

crates/contracts/src/sdk/issuance_validation/mod.rs

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::HashSet;
22

33
use simplicityhl::elements::confidential::Value as ConfidentialValue;
4-
use simplicityhl::elements::secp256k1_zkp::ZERO_TWEAK;
4+
use simplicityhl::elements::secp256k1_zkp::{SECP256K1, SecretKey, ZERO_TWEAK};
55
use simplicityhl::elements::{AssetId, Script, Transaction};
66

77
/// Constraints for verifying an issuance transaction.
@@ -20,11 +20,19 @@ pub struct IssuanceInputConstraints {
2020
/// Index into `tx.input`.
2121
pub input_idx: usize,
2222

23-
/// Destination and amount for the issued asset.
24-
pub issuance_destination: Option<(Script, u64)>,
25-
26-
/// Destination and amount for the reissuance token.
27-
pub reissuance_destination: Option<(Script, u64)>,
23+
/// Destination, amount, and optional blinding key for the issued asset.
24+
///
25+
/// The tuple is `(script, amount, blinding_key)`. When `blinding_key` is `Some`,
26+
/// confidential outputs are unblinded using it; if unblinding succeeds and the asset
27+
/// matches, the output is accounted for, otherwise it is skipped.
28+
pub issuance_destination: Option<(Script, u64, Option<SecretKey>)>,
29+
30+
/// Destination, amount, and optional blinding key for the reissuance token.
31+
///
32+
/// The tuple is `(script, amount, blinding_key)`. When `blinding_key` is `Some`,
33+
/// confidential outputs are unblinded using it; if unblinding succeeds and the asset
34+
/// matches, the output is accounted for, otherwise it is skipped.
35+
pub reissuance_destination: Option<(Script, u64, Option<SecretKey>)>,
2836
}
2937

3038
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
@@ -105,8 +113,12 @@ pub enum IssuanceVerificationError {
105113
///
106114
/// ## Confidentiality policy
107115
///
108-
/// - Outputs whose **asset is confidential** are ignored during verification (this verifier makes
109-
/// no claims about what may be hidden in confidential-asset outputs).
116+
/// - Each destination tuple carries an optional blinding key (the third element). When set,
117+
/// confidential outputs are unblinded using the key. If unblinding succeeds and the asset
118+
/// matches, the output is accounted for. If unblinding fails, the output is silently skipped.
119+
/// - When no blinding key is provided for a destination, outputs whose **asset is confidential**
120+
/// are ignored during verification (this verifier makes no claims about what may be hidden in
121+
/// confidential-asset outputs).
110122
/// - For constrained assets, any output with an **explicit matching asset** but a **non-explicit
111123
/// value** fails verification (cannot check exact amounts).
112124
///
@@ -241,13 +253,13 @@ fn verify_constrained_asset(
241253
tx: &Transaction,
242254
asset_id: AssetId,
243255
minted_amount: u64,
244-
destination: Option<&(Script, u64)>,
256+
destination: Option<&(Script, u64, Option<SecretKey>)>,
245257
input_idx: usize,
246258
kind: MintedConstraintKind,
247259
) -> Result<(), IssuanceVerificationError> {
248-
let (dest_script, expected_amount) = match destination {
249-
Some((s, amt)) => (Some(s), *amt),
250-
None => (None, 0),
260+
let (dest_script, expected_amount, blinder) = match destination {
261+
Some((s, amt, blinder)) => (Some(s), *amt, Option::from(blinder)),
262+
None => (None, 0, None),
251263
};
252264

253265
if minted_amount != expected_amount {
@@ -271,19 +283,20 @@ fn verify_constrained_asset(
271283
});
272284
}
273285

274-
verify_asset_destination(tx, asset_id, expected_amount, dest_script)
286+
verify_asset_destination(tx, asset_id, expected_amount, dest_script, blinder)
275287
}
276288

277289
fn verify_asset_destination(
278290
tx: &Transaction,
279291
asset_id: AssetId,
280292
expected_amount: u64,
281293
destination_script: Option<&Script>,
294+
blinding_key: Option<&SecretKey>,
282295
) -> Result<(), IssuanceVerificationError> {
283296
let mut sum_to_destination = 0u64;
284297

285298
for (vout, output) in tx.output.iter().enumerate() {
286-
match output.asset.explicit() {
299+
let resolved = match output.asset.explicit() {
287300
Some(out_asset) if out_asset == asset_id => {
288301
let Some(value) = output.value.explicit() else {
289302
return Err(
@@ -293,24 +306,30 @@ fn verify_asset_destination(
293306
},
294307
);
295308
};
309+
Some(value)
310+
}
311+
_ => blinding_key
312+
.and_then(|key| output.unblind(SECP256K1, *key).ok())
313+
.filter(|secrets| secrets.asset == asset_id)
314+
.map(|secrets| secrets.value),
315+
};
296316

297-
let Some(dest_script) = destination_script else {
298-
return Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput {
299-
vout,
300-
asset_id,
301-
});
302-
};
303-
304-
if output.script_pubkey != *dest_script {
305-
return Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput {
306-
vout,
307-
asset_id,
308-
});
309-
}
317+
if let Some(value) = resolved {
318+
let Some(dest_script) = destination_script else {
319+
return Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput {
320+
vout,
321+
asset_id,
322+
});
323+
};
310324

311-
sum_to_destination = sum_to_destination.saturating_add(value);
325+
if output.script_pubkey != *dest_script {
326+
return Err(IssuanceVerificationError::AssetAppearsInUnexpectedOutput {
327+
vout,
328+
asset_id,
329+
});
312330
}
313-
_ => {}
331+
332+
sum_to_destination = sum_to_destination.saturating_add(value);
314333
}
315334
}
316335

@@ -435,10 +454,10 @@ mod tests {
435454
let constraints = IssuanceTxConstraints {
436455
inputs: vec![IssuanceInputConstraints {
437456
input_idx: 0,
438-
issuance_destination: Some((issue_script, 50)),
439-
reissuance_destination: Some((token_script, 1)),
457+
issuance_destination: Some((issue_script, 50, None)),
458+
reissuance_destination: Some((token_script, 1, None)),
440459
}],
441-
allow_unconstrained_issuances: false,
460+
..Default::default()
442461
};
443462

444463
assert_eq!(verify_issuance(&tx, &constraints), Ok(()));
@@ -464,10 +483,10 @@ mod tests {
464483
let constraints = IssuanceTxConstraints {
465484
inputs: vec![IssuanceInputConstraints {
466485
input_idx: 0,
467-
issuance_destination: Some((issue_script, 10)),
468-
reissuance_destination: Some((token_script, 1)),
486+
issuance_destination: Some((issue_script, 10, None)),
487+
reissuance_destination: Some((token_script, 1, None)),
469488
}],
470-
allow_unconstrained_issuances: false,
489+
..Default::default()
471490
};
472491

473492
assert!(matches!(
@@ -495,10 +514,10 @@ mod tests {
495514
let constraints = IssuanceTxConstraints {
496515
inputs: vec![IssuanceInputConstraints {
497516
input_idx: 0,
498-
issuance_destination: Some((issue_script, 10)),
499-
reissuance_destination: Some((token_script, 1)),
517+
issuance_destination: Some((issue_script, 10, None)),
518+
reissuance_destination: Some((token_script, 1, None)),
500519
}],
501-
allow_unconstrained_issuances: false,
520+
..Default::default()
502521
};
503522

504523
assert!(matches!(
@@ -527,9 +546,9 @@ mod tests {
527546
inputs: vec![IssuanceInputConstraints {
528547
input_idx: 0,
529548
issuance_destination: None,
530-
reissuance_destination: Some((token_script, 1)),
549+
reissuance_destination: Some((token_script, 1, None)),
531550
}],
532-
allow_unconstrained_issuances: false,
551+
..Default::default()
533552
};
534553

535554
assert!(matches!(
@@ -547,7 +566,7 @@ mod tests {
547566
issuance_destination: None,
548567
reissuance_destination: None,
549568
}],
550-
allow_unconstrained_issuances: false,
569+
..Default::default()
551570
};
552571

553572
assert_eq!(
@@ -577,10 +596,10 @@ mod tests {
577596
let constraints_strict = IssuanceTxConstraints {
578597
inputs: vec![IssuanceInputConstraints {
579598
input_idx: 0,
580-
issuance_destination: Some((issue_script, 10)),
581-
reissuance_destination: Some((token_script, 1)),
599+
issuance_destination: Some((issue_script, 10, None)),
600+
reissuance_destination: Some((token_script, 1, None)),
582601
}],
583-
allow_unconstrained_issuances: false,
602+
..Default::default()
584603
};
585604

586605
assert_eq!(
@@ -621,15 +640,15 @@ mod tests {
621640
IssuanceInputConstraints {
622641
input_idx: 0,
623642
issuance_destination: None,
624-
reissuance_destination: Some((taproot_gen.address.script_pubkey(), 1)),
643+
reissuance_destination: Some((taproot_gen.address.script_pubkey(), 1, None)),
625644
},
626645
IssuanceInputConstraints {
627646
input_idx: 1,
628647
issuance_destination: None,
629-
reissuance_destination: Some((taproot_gen.address.script_pubkey(), 1)),
648+
reissuance_destination: Some((taproot_gen.address.script_pubkey(), 1, None)),
630649
},
631650
],
632-
allow_unconstrained_issuances: false,
651+
..Default::default()
633652
};
634653

635654
verify_issuance(&tx, &constraints).map_err(|e| format!("Verification failed: {e:?}"))?;

crates/contracts/src/sdk/storage/transfer_from_storage_address.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub fn transfer_asset_with_storage(
6969

7070
if change_amount > 0 {
7171
pst.add_output(Output::new_explicit(
72-
change_recipient_script.clone(),
72+
change_recipient_script,
7373
change_amount,
7474
fee_asset_id,
7575
None,

crates/contracts/src/smt_storage/build_witness.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub struct SMTWitness {
4545

4646
impl SMTWitness {
4747
#[must_use]
48-
pub fn new(
48+
pub const fn new(
4949
key: &u256,
5050
leaf: &u256,
5151
path_bits: u8,

crates/contracts/src/smt_storage/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ mod smt_storage_tests {
351351
let outpoint0 = OutPoint::new(Txid::from_slice(&[0; 32])?, 0);
352352
pst.add_input(Input::from_prevout(outpoint0));
353353
pst.add_output(Output::new_explicit(
354-
new_script_pubkey.clone(),
354+
new_script_pubkey,
355355
0,
356356
AssetId::default(),
357357
None,

crates/contracts/src/smt_storage/smt.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ enum TreeNode {
2222
/// The `hash` is typically calculated as `Hash(Left_Child_Hash || Right_Child_Hash)`.
2323
Branch {
2424
hash: u256,
25-
left: Box<TreeNode>,
26-
right: Box<TreeNode>,
25+
left: Box<Self>,
26+
right: Box<Self>,
2727
},
2828
}
2929

3030
impl TreeNode {
31-
pub fn get_hash(&self) -> u256 {
31+
pub const fn get_hash(&self) -> u256 {
3232
match self {
33-
TreeNode::Leaf { leaf_hash } => *leaf_hash,
34-
TreeNode::Branch { hash, .. } => *hash,
33+
Self::Leaf { leaf_hash } => *leaf_hash,
34+
Self::Branch { hash, .. } => *hash,
3535
}
3636
}
3737
}
@@ -113,7 +113,7 @@ impl SparseMerkleTree {
113113
}
114114

115115
/// Computes parent hash: `SHA256(left_child_hash || right_child_hash)`.
116-
fn calculate_hash(left: &mut TreeNode, right: &mut TreeNode) -> u256 {
116+
fn calculate_hash(left: &TreeNode, right: &TreeNode) -> u256 {
117117
let mut eng = sha256::Hash::engine();
118118
eng.input(&left.get_hash());
119119
eng.input(&right.get_hash());

crates/simplicityhl-core/src/tx_inclusion.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ use simplicityhl::elements::{Block, TxMerkleNode, Txid};
99
/// Merkle proof: (`transaction_index`, `sibling_hashes`)
1010
pub type MerkleProof = (usize, Vec<TxMerkleNode>);
1111

12-
/// Constructs a Merkle inclusion proof (Merkle branch) for a transaction TXID
13-
/// in a block, using Bitcoin consensus Merkle tree construction rules
12+
/// Constructs a Merkle inclusion proof (Merkle branch).
13+
///
14+
/// For a transaction TXID in a block, using Bitcoin consensus Merkle tree construction rules
1415
/// (pairwise double-SHA256 hashing with odd-hash duplication).
1516
///
1617
/// Liquid inherits the same Merkle tree semantics via the Elements codebase:
@@ -28,8 +29,9 @@ pub fn merkle_branch(tx: &Txid, block: &Block) -> Option<MerkleProof> {
2829
Some((tx_index, build_merkle_branch(tx_index, block)))
2930
}
3031

31-
/// Verifies a Merkle inclusion proof (Merkle branch) for a transaction TXID
32-
/// against the given Merkle root using Bitcoin consensus Merkle tree rules
32+
/// Verifies a Merkle inclusion proof (Merkle branch).
33+
///
34+
/// For a transaction TXID against the given Merkle root using Bitcoin consensus Merkle tree rules
3335
/// (pairwise double-SHA256 hashing with left/right ordering).
3436
///
3537
/// Liquid inherits the same Merkle tree semantics via the Elements codebase:
@@ -96,15 +98,14 @@ fn compute_merkle_root_from_branch(
9698
for leaf in branch {
9799
let mut eng = TxMerkleNode::engine();
98100

99-
res = if pos & 1 == 0 {
101+
if pos & 1 == 0 {
100102
eng.input(res.as_raw_hash().as_byte_array());
101103
eng.input(leaf.as_raw_hash().as_byte_array());
102-
TxMerkleNode::from_engine(eng)
103104
} else {
104105
eng.input(leaf.as_raw_hash().as_byte_array());
105106
eng.input(res.as_raw_hash().as_byte_array());
106-
TxMerkleNode::from_engine(eng)
107-
};
107+
}
108+
res = TxMerkleNode::from_engine(eng);
108109

109110
pos >>= 1;
110111
}

0 commit comments

Comments
 (0)