Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction builder #92

Merged
merged 19 commits into from
Aug 14, 2019
Merged

Transaction builder #92

merged 19 commits into from
Aug 14, 2019

Conversation

str4d
Copy link
Contributor

@str4d str4d commented Jul 16, 2019

Supports Sapling spends, Sapling outputs, and transparent (P2PKH and P2SH) outputs.

zcash_primitives/src/transaction/builder.rs Outdated Show resolved Hide resolved
zcash_primitives/src/transaction/builder.rs Outdated Show resolved Hide resolved
zcash_primitives/src/transaction/builder.rs Outdated Show resolved Hide resolved
zcash_primitives/src/transaction/builder.rs Show resolved Hide resolved
@Eirik0
Copy link
Contributor

Eirik0 commented Jul 24, 2019

When building I get a few warnings:

warning: variable does not need to be mutable
   --> sapling-crypto/src/jubjub/mod.rs:352:17
    |
352 |             for mut gen in tmp_params.pedersen_hash_generators.iter().cloned() {
    |                 ----^^^
    |                 |
    |                 help: remove this `mut`
    |
    = note: #[warn(unused_mut)] on by default

warning: variable does not need to be mutable
   --> sapling-crypto/src/jubjub/mod.rs:352:17
    |
352 |             for mut gen in tmp_params.pedersen_hash_generators.iter().cloned() {
    |                 ----^^^
    |                 |
    |                 help: remove this `mut`
    |
    = note: #[warn(unused_mut)] on by default

warning: variable does not need to be mutable
   --> sapling-crypto/src/circuit/uint32.rs:426:17
    |
426 |             let mut v = (0..32).map(|_| Boolean::constant(rng.gen())).collect::<Vec<_>>();
    |                 ----^
    |                 |
    |                 help: remove this `mut`

warning: variable does not need to be mutable
   --> sapling-crypto/src/circuit/uint32.rs:457:17
    |
457 |             let mut v = (0..32).map(|_| Boolean::constant(rng.gen())).collect::<Vec<_>>();
    |                 ----^
    |                 |
    |                 help: remove this `mut`

warning: variable does not need to be mutable
   --> sapling-crypto/src/circuit/pedersen_hash.rs:154:21
    |
154 |                 let mut input: Vec<bool> = (0..length).map(|_| rng.gen()).collect();
    |                     ----^^^^^
    |                     |
    |                     help: remove this `mut`

These may not be related to this PR, but are worth noting.

@str4d
Copy link
Contributor Author

str4d commented Jul 25, 2019

I think those warnings are due to NLL being activated on Rust 2015 edition in Rust 1.36.

@Eirik0
Copy link
Contributor

Eirik0 commented Jul 25, 2019

At least one of them shows up when building from master. I didn't spend too much time digging, but the NLL update makes sense. I don't mind if these are addressed as a separate issue.

@str4d
Copy link
Contributor Author

str4d commented Jul 25, 2019

I addressed @ebfull's comments by overhauling and extending the Amount struct. It is now used basically everywhere except inside Note, and enforces zatoshi range checks on instantiation.

Copy link
Collaborator

@ebfull ebfull left a comment

Choose a reason for hiding this comment

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

This looks great!

@str4d
Copy link
Contributor Author

str4d commented Jul 26, 2019

Rebased on master to fix merge conflicts caused by #91. I additionally had to modify various parts of this PR to account for the rand upgrade.

@str4d str4d requested a review from daira July 30, 2019 08:35
@@ -9,8 +9,13 @@ authors = [
bellman = { path = "../bellman" }
blake2b_simd = "0.5"
byteorder = "1"
directories = { version = "1", optional = true }
Copy link
Contributor Author

@str4d str4d Jul 30, 2019

Choose a reason for hiding this comment

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

I added version 1 as a dependency instead of version 2, because version 1 has fewer transitive dependencies.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it still maintained?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Version 2 is actively maintained, but has a dependency on redox_users, which pulls in a bunch of extraneous crates we don't actually need. It's unclear to me whether version 1 is maintained.

@@ -15,6 +15,7 @@ fpe = "0.1"
hex = "0.3"
lazy_static = "1"
pairing = { path = "../pairing" }
rand = "0.7"
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 added a dependency on rand separately from rand_core and rand_os because I wanted to access rand::seq::SliceRandom for shuffling the order of spends and outputs. I didn't bother replacing all the usages of rand_core and rand_os because it is unnecessary noise.

@ebfull ebfull mentioned this pull request Aug 6, 2019
pub const DEFAULT_FEE: Amount = Amount(10000);

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Amount(i64);
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if this had a doc-comment explaining the purpose of the newtype and explicitly declaring any invariants it maintains. For instance in from_i64 below, the constructor enforces some requirements on the input -- are those invariants that the type maintains, or just a requirement for that constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main reason for the newtype is type safety. The requirements in the constructor are somewhat tricky however. MAX_MONEY is not actually the maximum amount of money in the consensus rules due to rounding errors (and in fact that will decrease slightly with the Blossom NU). It is a consensus-critical validity check on visible values (transparent outputs, vpub_old, vpub_new, and valueBalance).

Now, we could have Amount maintain {-MAX_MONEY..MAX_MONEY} as an invariant, but what would this imply for the API? I'm not keen on burdening the API with error checks for every addition, given that the network consensus rules mean that any transaction created leveraging out-of-range values will be unmineable. But is a panic!() a reasonable way to handle this case? Wrapping definitely isn't (and is partly the motivation for checking MAX_MONEY in the constructor, so we know that any values instantiated are far below the u64 upper end, and thus can assume there will be no overflow during transaction building).

I'll add a doc-comment in any case.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should panic if addition or subtraction overflows the range -MAX_MONEY..MAX_MONEY.

/// Creates an Amount from an i64.
///
/// Returns an error if the amount is out of range.
pub fn from_i64(amount: i64, allow_negative: bool) -> Result<Self, ()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a minor nit, but I think that having an allow_negative: bool parameter isn't the best API design. The reason is that when reading caller code, all that appears is

let amt = Amount::from_i64(val, true)?;

or similar, and the reader has to remember that the true means that negative values are allowed, rather than true meaning that the value is required to be positive (and therefore negative values are disallowed), which would be an alternate way to construct the API.

There are two alternatives that I think would improve readability in the long run:

  1. (my preference) Split this code into two constructors whose names (strawman proposal) indicate their behaviour:
pub fn from_i64_nonnegative(amount: i64) -> Result<Self, ()>;
pub fn from_i64_allow_negative(amount: i64) -> Result<Self, ()>;
  1. Add an enum AmountValidation { NonNegative, NegativeAllowed } and replace the bool with an AmountValidation, so that caller code displays the validation criteria at the callsite:
let amt = Amount::from_i64(val, AmountValidation::NegativeAllowed)?;

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 is an excellent point. I also prefer the explicit constructors.

type Output = Amount;

fn add(self, rhs: Amount) -> Amount {
Amount(self.0 + rhs.0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to the invariant comment, what happens if I create two Amounts with MAX_MONEY - 1 and then add them? Is that a concern for this struct or is that the user's responsibility?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMHO that's the user's responsibility, from the perspective of this struct. The Amounts are either being constructed from visible fields in a transaction (in which case the consensus rules will reject the result, because at least one field contain the sum that is above MAX_MONEY, or they are being constructed from/for Notes (in which case we are relying on the shielded value balance enforced by the Pedersen commitments, and the commitments themselves being enforced by the circuits). Per my reply to the invariant comment, we could panic!() here.

This is more intuitive than a boolean flag for handling non-negative
Amounts stored in i64 values.
@str4d
Copy link
Contributor Author

str4d commented Aug 8, 2019

Pushed commits to address most of @hdevalence's comments. I'm still undecided on whether to enforce MAX_MONEY inside addition etc. with panic!().

Copy link
Contributor

@Eirik0 Eirik0 left a comment

Choose a reason for hiding this comment

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

I haven't finished going through this, but I had some suggestions regarding Error in builder.rs and wanted to get those posted.

BindingSig,
ChangeIsNegative(i64),
InvalidAddress,
InvalidAmount,
Copy link
Contributor

Choose a reason for hiding this comment

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

Some of these errors could include additional information, for example InvalidAmount could include the amount. This can be added later.

};

const DEFAULT_FEE: Amount = Amount(10000);
const DEFAULT_TX_EXPIRY_DELTA: u32 = 20;
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: This will need to be updated to account for Blossom.

zcash_primitives/src/transaction/builder.rs Outdated Show resolved Hide resolved
zcash_primitives/src/transaction/builder.rs Outdated Show resolved Hide resolved
zcash_primitives/src/transaction/builder.rs Outdated Show resolved Hide resolved
/// Creates an Amount from an i64.
///
/// Returns an error if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`.
pub fn from_i64(amount: i64) -> Result<Self, ()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to change the return type here and for similar functions from Result<Self, ()> to Option<Self>?

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 think I prefer this being an explicit (if unit) error. I know we are inconsistent about this in our API; we should open an issue for going through and refactoring everything to be internally consistent in how we decided to return errors vs None.

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be great to go through and make these consistent, but I am happy leaving this for now.

Requires impl PartialEq for Transaction, which is implemented as a TxId
comparison (relying on the invariant that Transaction is immutable).
Copy link
Contributor

@Eirik0 Eirik0 left a comment

Choose a reason for hiding this comment

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

utACK. In addition to reviewing this, I went through the cpp code and compared what I could. Everything looks great!

Copy link
Contributor

@daira daira left a comment

Choose a reason for hiding this comment

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

ut(ACK+cov) modulo minor comments.

@@ -704,6 +705,11 @@ pub extern "system" fn librustzcash_sapling_final_check(
binding_sig: *const [c_uchar; 64],
sighash_value: *const [c_uchar; 32],
) -> bool {
let value_balance = match Amount::from_i64(value_balance) {
Ok(vb) => vb,
Err(()) => return false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please file a ticket about better error reporting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opened #101.

zcash_primitives/src/legacy.rs Outdated Show resolved Hide resolved
}
}

impl Shl<OpCode> for Script {
Copy link
Contributor

Choose a reason for hiding this comment

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

sucks air through teeth at repeating this C++ antipattern

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just use a 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.

In order to support concatenating different types with the same builder method (instead of a custom method per type), I would need to implement my own trait, which would result in more code than this. I also felt there was a benefit in having this legacy part be as close to the C++ code as possible for reviewability.

to: PaymentAddress<Bls12>,
value: Amount,
memo: Option<Memo>,
) -> Result<Self, Error> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not seem to enforce (neither does PaymentAddress) that pkd is not the zero point. zcash/zcash#3827

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opened #102.

let pk_d = {
let dummy_ivk = Fs::random(&mut self.rng);
g_d.mul(dummy_ivk, &JUBJUB)
};
Copy link
Contributor

Choose a reason for hiding this comment

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

The protocol spec says: "As in Sprout, a dummy Sapling output note is constructed as normal but with zero value, and sent to a random shielded payment address." This looks correct.

if let Some(anchor) = self.anchor {
let witness_root: Fr = witness.root().into();
if witness_root != anchor {
return Err(Error::AnchorMismatch);
Copy link
Contributor

Choose a reason for hiding this comment

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

This error condition is not tested.

) -> Result<Self, Error> {
let g_d = match to.g_d(&JUBJUB) {
Some(g_d) => g_d,
None => return Err(Error::InvalidAddress),
Copy link
Contributor

Choose a reason for hiding this comment

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

This error condition is not tested.

} else {
self.anchor = Some(witness.root().into())
}
let witness = witness.path().ok_or(Error::InvalidWitness)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

This error condition is not tested.

self.mtx.binding_sig = Some(
prover
.binding_sig(&mut ctx, self.mtx.value_balance, &sighash)
.map_err(|()| Error::BindingSig)?,
Copy link
Contributor

Choose a reason for hiding this comment

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

I am satisfied that this cannot occur for the local TxProver. We do effectively test it for the mock TxProver (which does not generate a valid binding signature).

pub const DEFAULT_FEE: Amount = Amount(10000);

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Amount(i64);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should panic if addition or subtraction overflows the range -MAX_MONEY..MAX_MONEY.

Co-Authored-By: Daira Hopwood <daira@jacaranda.org>
@str4d str4d merged commit 52ea437 into zcash:master Aug 14, 2019
@str4d str4d deleted the transaction-builder branch August 14, 2019 00:11
@str4d str4d added this to the v0.1.0 milestone Aug 22, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants