-
Notifications
You must be signed in to change notification settings - Fork 106
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
Redesign Sapling data model for V5 shared anchor and spends #2021
Conversation
I think we should go ahead with this redesign. It is more clear than what we have now and matches better what we need to implement. |
The shared anchor is only present if there are any spends. As part of this change, delete the manual PartialEq impl and its tests, because we can derive PartialEq now.
Interactive rename using the following commands: ```sh fastmod Spends SpendsAndMaybeOutputs fastmod NoSpends JustOutputs ```
This vector wrapper ensures that it always contains at least one element.
dd6b0a3
to
673b95d
Compare
Also update the RFC to use AtLeastOne for Orchard.
5536af6
to
cc8292f
Compare
What do you think of the |
@@ -128,11 +128,11 @@ struct FieldNotPresent; | |||
|
|||
impl AnchorVariant for PerSpendAnchor { | |||
type Shared = FieldNotPresent; | |||
type PerSpend = tree::Root; | |||
type PerSpend = sapling::tree::Root; |
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.
👍
} | ||
|
||
impl AnchorVariant for SharedAnchor { | ||
type Shared = tree::Root; | ||
type Shared = sapling::tree::Root; |
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.
👍
Everyone has read through this, so I'm going to merge it, so we can get on with the work that it's blocking. |
If there are no spends and no outputs: | ||
* in v4, the value_balance is fixed to zero | ||
* in v5, the value balance field is not present | ||
* in both versions, the binding_sig field is not present |
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.
👍
/// If there are no spends, there must not be a shared | ||
/// anchor. |
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 Transaction::V5, if there are any spends, | ||
/// there must also be a shared spend anchor. |
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.
👍
pub rest_outputs: Vec<Output>, | ||
/// In V5 transactions, also contains a shared anchor, if there are any | ||
/// spends. | ||
pub transfers: TransferData<AnchorV>, |
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 anchor is the root of the Sapling note commitment tree in a previous | ||
/// block. This root should be in the best chain for a transaction to be | ||
/// mined, and it must be in the relevant chain for a transaction to be | ||
/// valid. |
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.
👍
|
||
/// Maybe some outputs (can be empty). | ||
/// | ||
/// Use the [`ShieldedData::outputs`] method to get an iterator over 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.
👍
|
||
/// At least one spend. | ||
/// | ||
/// Use the [`ShieldedData::spends`] method to get an iterator over 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.
👍
} | ||
.into_iter() | ||
.chain(self.rest_spends.iter()) | ||
self.transfers.spends() |
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.
👍
} | ||
.into_iter() | ||
.chain(self.rest_outputs.iter()) | ||
self.transfers.outputs() |
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.
👍
/// Provide the shared anchor for this transaction, if present. | ||
/// | ||
/// The shared anchor is only present if: | ||
/// * there is at least one spend, and | ||
/// * this is a `V5` transaction. | ||
pub fn shared_anchor(&self) -> Option<AnchorV::Shared> { | ||
self.transfers.shared_anchor() |
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 Sapling `value_balance` field is optional in `Transaction::V5`, but | ||
/// required in `Transaction::V4`. In both cases, if there is no `ShieldedData`, | ||
/// then the field value must be zero. Therefore, only need to store | ||
/// then the field value must be zero. Therefore, we only need to store | ||
/// `value_balance` when there is some Sapling `ShieldedData`. | ||
/// | ||
/// In `Transaction::V4`, each `Spend` has its own anchor. In `Transaction::V5`, | ||
/// there is a single `shared_anchor` for the entire transaction. This | ||
/// structural difference is modeled using the `AnchorVariant` type trait. | ||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
/// there is a single `shared_anchor` for the entire transaction, which is only | ||
/// present when there is at least one spend. These structural differences are | ||
/// modeled using the `AnchorVariant` type trait and `TransferData` enum. |
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 awkward construction avoids returning a newtype struct or | ||
// type-erased boxed iterator |
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.
👍
|
||
/// Iterate over the [`Output`]s for this transaction. | ||
pub fn outputs(&self) -> impl Iterator<Item = &Output> { | ||
use TransferData::*; |
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.
If we're importing this inside several methods, should we just import it at the top of the module?
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.
So this is a bit of a weird one, because TransferData
is an enum.
Sometimes you want the fully qualified name, if it's not obvious from the context.
match self { | ||
SpendsAndMaybeOutputs { maybe_outputs, .. } => maybe_outputs, | ||
JustOutputs { outputs, .. } => outputs.as_vec(), | ||
} | ||
.iter() |
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.
🕺
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.
Yeah it's much nicer to have an inner checked Vec<T>
impl rather than (T, Vec<T>)
.
match self { | ||
SpendsAndMaybeOutputs { shared_anchor, .. } => Some(shared_anchor.clone()), | ||
JustOutputs { .. } => 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.
🕺
// CORRECTNESS | ||
// | ||
// All conversions to `AtLeastOne<T>` must go through `TryFrom<Vec<T>>`, | ||
// so that the type constraint is satisfied. |
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.
👍
/// Deserialize an `AtLeastOne` vector, where the number of items is set by a | ||
/// compactsize prefix in the data. This is the most common format in Zcash. | ||
impl<T: ZcashDeserialize + TrustedPreallocate> ZcashDeserialize for AtLeastOne<T> { | ||
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> { | ||
let v: Vec<T> = (&mut reader).zcash_deserialize_into()?; | ||
v.try_into() | ||
} | ||
} |
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.
👍
/// Serialize an `AtLeastOne` vector as a compactsize number of items, then the | ||
/// items. This is the most common format in Zcash. | ||
impl<T: ZcashSerialize> ZcashSerialize for AtLeastOne<T> { | ||
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> { | ||
self.as_vec().zcash_serialize(&mut writer) | ||
} | ||
} |
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.
👍
type Parameters = (); | ||
|
||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { | ||
// TODO: add an extra spend or output using Either, and stop using filter_map |
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.
👍
@@ -48,4 +48,10 @@ impl<P: ZkSnarkProof> JoinSplitData<P> { | |||
pub fn joinsplits(&self) -> impl Iterator<Item = &JoinSplit<P>> { | |||
std::iter::once(&self.first).chain(self.rest.iter()) | |||
} | |||
|
|||
/// Iterate over the [`Nullifier`]s in `self`. | |||
pub fn nullifiers(&self) -> impl Iterator<Item = &Nullifier> { |
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.
👍
// `TransferData` ensures this field is only present when there is at | ||
// least one spend. |
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.
👍
let shared_anchor = if spends_count > 0 { | ||
Some(reader.read_32_bytes()?.into()) | ||
} else { | ||
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.
👍
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.
✅
Motivation
In V5 transactions, the shared anchor is only present if there are any spends.
This requires an extra
TransferData
type to model correctly.(This change was made after the NU5 spec audit.)
Solution
Update the design and tests to include:
TransferData
type which makes sure that the shared anchor is only present when there are spendsAtLeastOne
vector wrapper, which makes sure its inner vector always contains at least one elementAs part of this change, delete the manual PartialEq impl and its tests, because we can derive PartialEq now.
The code in this pull request has:
Review
Both @oxarbitrage and @dconnolly should have a look at the new design, these changes are pretty major.
Related Issues
#1829 implement V5 transaction for sapling
#2020 PR for V5 serialization for sapling
Follow Up Work
#2028 use
AtLeastOne
for other types, likeJoinSplitData
,Block.transaction
, and some network messages