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

Redesign Sapling data model for V5 shared anchor and spends #2021

Merged
merged 6 commits into from
Apr 20, 2021

Conversation

teor2345
Copy link
Contributor

@teor2345 teor2345 commented Apr 16, 2021

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:

  • a TransferData type which makes sure that the shared anchor is only present when there are spends
  • an AtLeastOne vector wrapper, which makes sure its inner vector always contains at least one element

As 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:

  • Documentation Comments
  • Unit Tests and Property Tests

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, like JoinSplitData, Block.transaction, and some network messages

@teor2345 teor2345 added C-design Category: Software design work A-consensus Area: Consensus rule updates A-rust Area: Updates to Rust code C-enhancement Category: This is an improvement C-cleanup Category: This is a cleanup NU-5 Network Upgrade: NU5 specific tasks P-Medium labels Apr 16, 2021
@teor2345 teor2345 added this to the 2021 Sprint 7 milestone Apr 16, 2021
@teor2345 teor2345 self-assigned this Apr 16, 2021
@oxarbitrage
Copy link
Contributor

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.
@teor2345 teor2345 force-pushed the redesign-shared-anchor-spend branch from dd6b0a3 to 673b95d Compare April 19, 2021 02:03
Also update the RFC to use AtLeastOne for Orchard.
@teor2345 teor2345 force-pushed the redesign-shared-anchor-spend branch from 5536af6 to cc8292f Compare April 19, 2021 02:35
@teor2345 teor2345 changed the title Redesign Sapling data model for V5 shared anchor and spends (do not merge) Redesign Sapling data model for V5 shared anchor and spends Apr 19, 2021
@teor2345 teor2345 marked this pull request as ready for review April 19, 2021 02:42
@teor2345
Copy link
Contributor Author

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.

What do you think of the AtLeastOne change?
https://github.com/ZcashFoundation/zebra/pull/2021/files#diff-a377e69ec608cdeb4b2c5bf8159152daf73ad6584e960882525828bf79bfdc96R174

@@ -128,11 +128,11 @@ struct FieldNotPresent;

impl AnchorVariant for PerSpendAnchor {
type Shared = FieldNotPresent;
type PerSpend = tree::Root;
type PerSpend = sapling::tree::Root;
Copy link
Contributor

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@teor2345
Copy link
Contributor Author

Everyone has read through this, so I'm going to merge it, so we can get on with the work that it's blocking.

@teor2345 teor2345 merged commit 53779d2 into main Apr 20, 2021
@teor2345 teor2345 deleted the redesign-shared-anchor-spend branch April 20, 2021 06:22
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
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +178 to +179
/// If there are no spends, there must not be a shared
/// anchor.
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +170 to +171
/// In Transaction::V5, if there are any spends,
/// there must also be a shared spend anchor.
Copy link
Contributor

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>,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +125 to +128
/// 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.
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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()
Copy link
Contributor

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()
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +206 to +212
/// 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()
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines 74 to +82
/// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +274 to +275
// this awkward construction avoids returning a newtype struct or
// type-erased boxed iterator
Copy link
Contributor

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::*;
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines +283 to +287
match self {
SpendsAndMaybeOutputs { maybe_outputs, .. } => maybe_outputs,
JustOutputs { outputs, .. } => outputs.as_vec(),
}
.iter()
Copy link
Contributor

Choose a reason for hiding this comment

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

🕺

Copy link
Contributor Author

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>).

Comment on lines +298 to +301
match self {
SpendsAndMaybeOutputs { shared_anchor, .. } => Some(shared_anchor.clone()),
JustOutputs { .. } => None,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🕺

Comment on lines +77 to +80
// CORRECTNESS
//
// All conversions to `AtLeastOne<T>` must go through `TryFrom<Vec<T>>`,
// so that the type constraint is satisfied.
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +35 to +42
/// 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()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +43 to +49
/// 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)
}
}
Copy link
Contributor

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
Copy link
Contributor

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> {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +123 to +124
// `TransferData` ensures this field is only present when there is at
// least one spend.
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines +167 to +171
let shared_anchor = if spends_count > 0 {
Some(reader.read_32_bytes()?.into())
} else {
None
};
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@dconnolly dconnolly left a comment

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-consensus Area: Consensus rule updates A-rust Area: Updates to Rust code C-cleanup Category: This is a cleanup C-design Category: Software design work C-enhancement Category: This is an improvement NU-5 Network Upgrade: NU5 specific tasks
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants