-
Notifications
You must be signed in to change notification settings - Fork 255
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
Conversation
700326d
to
c5f92f8
Compare
When building I get a few warnings:
These may not be related to this PR, but are worth noting. |
I think those warnings are due to NLL being activated on Rust 2015 edition in Rust 1.36. |
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. |
I addressed @ebfull's comments by overhauling and extending the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great!
An implementation using local parameters is provided in the zcash_proofs crate.
Overrides the shift-left operator for pushing opcodes onto the Script, matching the notation used in zcashd.
This enables zcash_proofs to be compiled to WASM, which the directories crate doesn't support.
This helps to ensure type-safety of values that are required to satisfy zatoshi range bounds.
ecb4279
to
59ed258
Compare
Rebased on master to fix merge conflicts caused by #91. I additionally had to modify various parts of this PR to account for the |
@@ -9,8 +9,13 @@ authors = [ | |||
bellman = { path = "../bellman" } | |||
blake2b_simd = "0.5" | |||
byteorder = "1" | |||
directories = { version = "1", optional = true } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it still maintained?
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
pub const DEFAULT_FEE: Amount = Amount(10000); | ||
|
||
#[derive(Clone, Copy, Debug, PartialEq)] | ||
pub struct Amount(i64); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, ()> { |
There was a problem hiding this comment.
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:
- (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, ()>;
- Add an
enum AmountValidation { NonNegative, NegativeAllowed }
and replace thebool
with anAmountValidation
, so that caller code displays the validation criteria at the callsite:
let amt = Amount::from_i64(val, AmountValidation::NegativeAllowed)?;
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 Amount
s with MAX_MONEY - 1
and then add them? Is that a concern for this struct or is that the user's responsibility?
There was a problem hiding this comment.
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 Amount
s 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 Note
s (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.
Pushed commits to address most of @hdevalence's comments. I'm still undecided on whether to enforce |
There was a problem hiding this 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, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
/// 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, ()> { |
There was a problem hiding this comment.
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>
?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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).
There was a problem hiding this 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!
There was a problem hiding this 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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #101.
} | ||
} | ||
|
||
impl Shl<OpCode> for Script { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) | ||
}; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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)?; |
There was a problem hiding this comment.
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)?, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 <[email protected]>
Supports Sapling spends, Sapling outputs, and transparent (P2PKH and P2SH) outputs.