Skip to content

Commit dc861af

Browse files
PR471 review: various comments (#228)
Address the following review comments: r2542313926 r2550287842 r2602977967 r2543702226 r2542907997 r2585555720 r2602558531
1 parent e893aac commit dc861af

File tree

6 files changed

+94
-37
lines changed

6 files changed

+94
-37
lines changed

book/src/design/actions.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,9 @@ we do not verify that the corresponding spent note commitment is part of the Mer
2929

3030
## Split notes for OrchardZSA
3131

32-
For OrchardZSA, if the number of inputs exceeds the number of outputs,
33-
we use dummy output notes (as in Orchard) to fill all actions.
34-
Conversely, if the number of outputs exceeds the number of inputs, we use split notes to fill the actions.
3532
In OrchardZSA, ensuring that the AssetBase is correctly created is crucial.
36-
For this reason, split notes are used instead of dummy spent notes.
33+
For this reason, when inputs and outputs are unbalanced, actions are completed using
34+
split notes for inputs or dummy notes for outputs.
3735
Split notes are essentially duplicates of actual spent notes,
3836
but with the following differences:
3937
- The nullifier is randomized to prevent it from being treated as double-spending.

src/builder.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use crate::{
2222
note::{AssetBase, ExtractedNoteCommitment, Note, Nullifier, Rho, TransmittedNoteCiphertext},
2323
primitives::redpallas::{self, Binding, SpendAuth},
2424
primitives::{OrchardDomain, OrchardPrimitives},
25-
sighash_versioning::{VerBindingSig, VerSpendAuthSig},
25+
sighash_versioning::{OrchardSighashVersion, VerBindingSig, VerSpendAuthSig},
2626
tree::{Anchor, MerklePath},
2727
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
2828
Proof,
@@ -699,6 +699,11 @@ impl Builder {
699699
return Err(BuildError::Burn(BurnError::ZeroAmount));
700700
}
701701

702+
// Check that the burn amount fits in u63
703+
if value.inner() >= (1u64 << 63) {
704+
return Err(BuildError::Burn(BurnError::InvalidAmount));
705+
}
706+
702707
match self.burn.entry(asset) {
703708
Entry::Occupied(_) => Err(BuildError::Burn(BurnError::DuplicateAsset)),
704709
Entry::Vacant(entry) => {
@@ -1067,7 +1072,7 @@ fn build_bundle<B, R: RngCore>(
10671072
/// Marker trait representing bundle signatures in the process of being created.
10681073
pub trait InProgressSignatures: fmt::Debug {
10691074
/// The authorization type of an Orchard action in the process of being authorized.
1070-
type SpendAuth: fmt::Debug + Clone;
1075+
type SpendAuth: fmt::Debug;
10711076
}
10721077

10731078
/// Marker for a bundle in the process of being built.
@@ -1296,6 +1301,9 @@ impl<P: fmt::Debug, V, Pr: OrchardPrimitives> Bundle<InProgress<P, PartiallyAuth
12961301
}
12971302

12981303
fn append_signature(self, signature: &VerSpendAuthSig) -> Result<Self, BuildError> {
1304+
if signature.version() != &OrchardSighashVersion::V0 {
1305+
return Err(BuildError::InvalidExternalSignature);
1306+
}
12991307
let mut signature_valid_for = 0usize;
13001308
let bundle = self.map_authorization(
13011309
&mut signature_valid_for,

src/bundle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ impl Flags {
202202
/// Defines the authorization type of an Orchard bundle.
203203
pub trait Authorization: fmt::Debug {
204204
/// The authorization type of an Orchard action.
205-
type SpendAuth: fmt::Debug + Clone;
205+
type SpendAuth: fmt::Debug;
206206
}
207207

208208
/// A bundle of actions to be applied to the ledger.

src/bundle/burn_validation.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ pub enum BurnError {
1717
NativeAsset,
1818
/// Cannot burn an asset with a zero value.
1919
ZeroAmount,
20+
/// Burn amount does not fit in u63.
21+
InvalidAmount,
2022
}
2123

22-
/// Validates burn for a bundle by ensuring each asset is unique, non-native, and has a non-zero value.
24+
/// Validates burn for a bundle by ensuring each asset is unique, non-native, fit in u63 and has a
25+
/// non-zero value.
2326
///
2427
/// Each burn element is represented as a tuple of `AssetBase` and `NoteValue` (value for the burn).
2528
///
@@ -32,6 +35,7 @@ pub enum BurnError {
3235
/// Returns a `BurnError` if:
3336
/// * Any asset in the `burn` vector is native (`BurnError::NativeAsset`).
3437
/// * Any asset in the `burn` vector has a zero value (`BurnError::ZeroAmount`).
38+
/// * Any burn amount in the `burn` vector is out of the u63 range (`BurnError::InvalidAmount`).
3539
/// * Any asset in the `burn` vector is not unique (`BurnError::DuplicateAsset`).
3640
pub fn validate_bundle_burn(burn: &[(AssetBase, NoteValue)]) -> Result<(), BurnError> {
3741
let mut burn_set = BTreeSet::new();
@@ -43,6 +47,9 @@ pub fn validate_bundle_burn(burn: &[(AssetBase, NoteValue)]) -> Result<(), BurnE
4347
if value.inner() == 0 {
4448
return Err(BurnError::ZeroAmount);
4549
}
50+
if value.inner() >= (1u64 << 63) {
51+
return Err(BurnError::InvalidAmount);
52+
}
4653
if !burn_set.insert(*asset) {
4754
return Err(BurnError::DuplicateAsset);
4855
}
@@ -59,6 +66,9 @@ impl fmt::Display for BurnError {
5966
BurnError::ZeroAmount => {
6067
write!(f, "Cannot burn an asset with a zero value.")
6168
}
69+
BurnError::InvalidAmount => {
70+
write!(f, "Burn amount must fit in u63.")
71+
}
6272
}
6373
}
6474
}
@@ -148,4 +158,20 @@ mod tests {
148158

149159
assert_eq!(result, Err(BurnError::ZeroAmount));
150160
}
161+
162+
#[test]
163+
fn validate_bundle_burn_invalid_amount() {
164+
let mut rng = OsRng;
165+
let mut used = BTreeSet::new();
166+
167+
let bundle_burn = vec![
168+
burn_tuple_unique(&mut rng, &mut used, 10),
169+
burn_tuple_unique(&mut rng, &mut used, 1u64 << 63),
170+
burn_tuple_unique(&mut rng, &mut used, 10),
171+
];
172+
173+
let result = validate_bundle_burn(&bundle_burn);
174+
175+
assert_eq!(result, Err(BurnError::InvalidAmount));
176+
}
151177
}

src/note/asset_base.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use core::cmp::Ordering;
22
use core::hash::{Hash, Hasher};
3-
use group::GroupEncoding;
3+
use group::{Group, GroupEncoding};
44
use pasta_curves::{arithmetic::CurveExt, pallas};
55
use subtle::{Choice, ConstantTimeEq, CtOption};
66

@@ -9,9 +9,6 @@ use crate::constants::fixed_bases::{NATIVE_ASSET_BASE_V_BYTES, VALUE_COMMITMENT_
99
#[cfg(test)]
1010
use rand_core::CryptoRngCore;
1111

12-
#[cfg(any(test, feature = "zsa-issuance"))]
13-
use group::Group;
14-
1512
#[cfg(feature = "zsa-issuance")]
1613
use {
1714
crate::constants::fixed_bases::ZSA_ASSET_BASE_PERSONALIZATION,
@@ -100,8 +97,12 @@ pub const ZSA_ASSET_DIGEST_PERSONALIZATION: &[u8; 16] = b"ZSA-Asset-Digest";
10097

10198
impl AssetBase {
10299
/// Deserialize the AssetBase from a byte array.
100+
///
101+
/// Returns `None` if the byte encoding is invalid or if it corresponds
102+
/// to the identity point.
103103
pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
104-
pallas::Point::from_bytes(bytes).map(AssetBase)
104+
pallas::Point::from_bytes(bytes)
105+
.and_then(|asset| CtOption::new(AssetBase(asset), !asset.is_identity()))
105106
}
106107

107108
/// Serialize the AssetBase to its canonical byte representation.

src/value.rs

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ mod tests {
490490
};
491491
use crate::{
492492
note::asset_base::testing::arb_asset_base, note::AssetBase, primitives::redpallas,
493+
value::NoteValue,
493494
};
494495

495496
fn negate_value_sum(value: ValueSum) -> ValueSum {
@@ -503,20 +504,46 @@ mod tests {
503504
ValueSum::from_magnitude_sign(magnitude, neg_sign)
504505
}
505506

507+
fn arb_asset_values_balanced_per_asset(
508+
bound: NoteValue,
509+
n_assets: usize,
510+
) -> impl Strategy<Value = Vec<(ValueSum, ValueCommitTrapdoor, AssetBase)>> {
511+
(
512+
prop::collection::vec(arb_asset_base(), n_assets),
513+
prop::collection::vec(arb_value_sum_bounded(bound), n_assets * 4),
514+
prop::collection::vec(arb_trapdoor(), n_assets * 5),
515+
)
516+
.prop_map(move |(assets, vals, traps)| {
517+
let mut traps = traps.into_iter();
518+
let mut out = Vec::with_capacity(n_assets * 5);
519+
520+
for (asset, four_values) in assets.into_iter().zip(vals.chunks_exact(4)) {
521+
let sum = four_values
522+
.iter()
523+
.cloned()
524+
.sum::<Result<ValueSum, OverflowError>>()
525+
.expect("we generate values that won't overflow");
526+
527+
let fifth = negate_value_sum(sum);
528+
529+
// Four random entries
530+
for value in four_values.iter().cloned() {
531+
out.push((value, traps.next().expect("enough trapdoors"), asset));
532+
}
533+
534+
// One balancing entry so that the per-asset balance is zero
535+
out.push((fifth, traps.next().expect("enough trapdoors"), asset));
536+
}
537+
538+
out
539+
})
540+
}
541+
506542
fn check_binding_signature(
507543
native_values: &[(ValueSum, ValueCommitTrapdoor)],
508544
arb_values: &[(ValueSum, ValueCommitTrapdoor, AssetBase)],
509-
neg_trapdoors: &[ValueCommitTrapdoor],
510545
arb_values_to_burn: &[(ValueSum, ValueCommitTrapdoor, AssetBase)],
511546
) {
512-
// for each arb value, create a negative value with a different trapdoor
513-
let neg_arb_values: Vec<_> = arb_values
514-
.iter()
515-
.cloned()
516-
.zip(neg_trapdoors.iter().cloned())
517-
.map(|((value, _, asset), rcv)| (negate_value_sum(value), rcv, asset))
518-
.collect();
519-
520547
let native_value_balance = native_values
521548
.iter()
522549
.map(|(value, _)| value)
@@ -529,13 +556,7 @@ mod tests {
529556
.map(|(value_sum, trapdoor)| (*value_sum, trapdoor.clone(), AssetBase::native()))
530557
.collect();
531558

532-
let values = [
533-
&native_values_with_asset,
534-
arb_values,
535-
&neg_arb_values,
536-
arb_values_to_burn,
537-
]
538-
.concat();
559+
let values = [&native_values_with_asset, arb_values, arb_values_to_burn].concat();
539560

540561
let bsk = values
541562
.iter()
@@ -571,20 +592,23 @@ mod tests {
571592
prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor()), n_values)
572593
)
573594
),
574-
(asset_values, neg_trapdoors) in (1usize..10).prop_flat_map(|n_values|
575-
(arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64).prop_flat_map(move |bound|
576-
prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor(), arb_asset_base()), n_values)
577-
), prop::collection::vec(arb_trapdoor(), n_values))
595+
// An Orchard ZSA transaction is valid only if the balance of each custom asset is zero.
596+
// Accordingly, for each custom asset we generate 5 random ValueSum values (possibly negative)
597+
// that sum to zero.
598+
asset_values in (1usize..10).prop_flat_map(|n_assets|
599+
arb_note_value_bounded(MAX_NOTE_VALUE / (n_assets as u64 * 5)).prop_flat_map(move |bound|
600+
arb_asset_values_balanced_per_asset(bound, n_assets)
601+
)
578602
),
579603
burn_values in (1usize..10).prop_flat_map(|n_values|
580604
arb_note_value_bounded(MAX_NOTE_VALUE / n_values as u64)
581605
.prop_flat_map(move |bound| prop::collection::vec((arb_value_sum_bounded(bound), arb_trapdoor(), arb_asset_base()), n_values))
582606
)
583607
) {
584-
check_binding_signature(&native_values, &[], &[], &[]);
585-
check_binding_signature(&native_values, &[], &[], &burn_values);
586-
check_binding_signature(&native_values, &asset_values, &neg_trapdoors, &[]);
587-
check_binding_signature(&native_values, &asset_values, &neg_trapdoors, &burn_values);
608+
check_binding_signature(&native_values, &[], &[]);
609+
check_binding_signature(&native_values, &[], &burn_values);
610+
check_binding_signature(&native_values, &asset_values, &[]);
611+
check_binding_signature(&native_values, &asset_values, &burn_values);
588612
}
589613
}
590614
}

0 commit comments

Comments
 (0)