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

Serialize/Deserialize sapling data in V5 #1860

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions zebra-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pub enum Transaction {
/// The latest block height that this transaction can be added to the chain.
expiry_height: block::Height,
/// The net value of Sapling spend transfers minus output transfers.
value_balance: Amount,
sapling_value_balance: Amount,
/// The JoinSplit data for this transaction, if any.
joinsplit_data: Option<JoinSplitData<Groth16Proof>>,
/// The shielded data for this transaction, if any.
Expand All @@ -112,6 +112,10 @@ pub enum Transaction {
inputs: Vec<transparent::Input>,
/// The transparent outputs from the transaction.
outputs: Vec<transparent::Output>,
/// The shielded data for this transaction, if any.
shielded_data: Option<ShieldedData>,
/// The net value of Sapling spend transfers minus output transfers.
sapling_value_balance: Amount,
/// The rest of the transaction as bytes
rest: Vec<u8>,
},
Expand Down Expand Up @@ -222,21 +226,26 @@ impl Transaction {
// This function returns a boxed iterator because the different
// transaction variants end up having different iterator types
match self {
// JoinSplits with Groth Proofs
// Transactions with ShieldedData
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Transaction::V4 {
shielded_data: Some(shielded_data),
..
} => Box::new(shielded_data.nullifiers()),
Transaction::V5 { .. } => {
unimplemented!("v5 transaction format as specified in ZIP-225")
}
// No JoinSplits
Transaction::V5 {
shielded_data: Some(shielded_data),
..
} => Box::new(shielded_data.nullifiers()),
// No ShieldedData
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 {
shielded_data: None,
..
}
| Transaction::V5 {
shielded_data: None,
..
} => Box::new(std::iter::empty()),
}
}
Expand Down
18 changes: 15 additions & 3 deletions zebra-chain/src/transaction/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ impl Transaction {
outputs,
lock_time,
expiry_height,
value_balance,
sapling_value_balance,
shielded_data,
joinsplit_data,
)| Transaction::V4 {
inputs,
outputs,
lock_time,
expiry_height,
value_balance,
sapling_value_balance,
shielded_data,
joinsplit_data,
},
Expand All @@ -111,14 +111,26 @@ impl Transaction {
any::<block::Height>(),
transparent::Input::vec_strategy(ledger_state, 10),
vec(any::<transparent::Output>(), 0..10),
option::of(any::<ShieldedData>()),
any::<Amount>(),
any::<Vec<u8>>(),
)
.prop_map(
|(lock_time, expiry_height, inputs, outputs, rest)| Transaction::V5 {
|(
lock_time,
expiry_height,
inputs,
outputs,
shielded_data,
sapling_value_balance,
rest,
)| Transaction::V5 {
lock_time,
expiry_height,
inputs,
outputs,
shielded_data,
sapling_value_balance,
rest,
},
)
Expand Down
140 changes: 98 additions & 42 deletions zebra-chain/src/transaction/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,65 @@ impl<P: ZkSnarkProof> ZcashDeserialize for Option<JoinSplitData<P>> {
}
}

struct SigShieldedData<'a> {
sig: bool,
shielded_data: &'a Option<ShieldedData>,
}
Comment on lines +69 to +72
Copy link
Contributor

Choose a reason for hiding this comment

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

transaction::ShieldedData includes binding_sig, for the Sapling data I'm not sure why this type is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is needed for the order of the fields inside a transaction. In V4 transactions we have the spends and outputs for sapling then the joinsplits and then the binding sig. This is explained in https://github.com/ZcashFoundation/zebra/blob/main/zebra-chain/src/transaction/serialize.rs#L147-L153 and can be verified by checking the fields of the V4 transaction in the spec.

In the other hand in the V5 transactions the binding sig is intermediately after the sapling spends and outputs so it can be serialized right there as there is nothing in the middle.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a doc comment for this type, that explains why it's needed, and how it should be used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, it looks like we can't use this type, because ShieldedData is different in V4 and V5 transactions.


impl ZcashSerialize for SigShieldedData<'_> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
let sig = self.sig;
let shielded_data = self.shielded_data;
match shielded_data {
None => {
// Signal no shielded spends and no shielded outputs.
Copy link
Contributor

@teor2345 teor2345 Mar 7, 2021

Choose a reason for hiding this comment

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

Suggested change
// Signal no shielded spends and no shielded outputs.
// There are no shielded spends and no shielded outputs.
// Since all other fields are omitted, the serialization is the same in V4 and V5 transactions.

writer.write_compactsize(0)?;
writer.write_compactsize(0)?;
}
Some(shielded_data) => {
writer.write_compactsize(shielded_data.spends().count() as u64)?;
for spend in shielded_data.spends() {
spend.zcash_serialize(&mut writer)?;
}
writer.write_compactsize(shielded_data.outputs().count() as u64)?;
for output in shielded_data.outputs() {
output.zcash_serialize(&mut writer)?;
}
if sig {
writer.write_all(&<[u8; 64]>::from(shielded_data.binding_sig)[..])?
}
}
}
Ok(())
}
}

fn deserialize_shielded_data<R: io::Read>(
mut reader: R,
mut shielded_spends: Vec<sapling::Spend>,
mut shielded_outputs: Vec<sapling::Output>,
) -> Result<Option<ShieldedData>, SerializationError> {
use futures::future::Either::*;

if !shielded_spends.is_empty() {
Ok(Some(ShieldedData {
first: Left(shielded_spends.remove(0)),
rest_spends: shielded_spends,
rest_outputs: shielded_outputs,
binding_sig: reader.read_64_bytes()?.into(),
}))
} else if !shielded_outputs.is_empty() {
Ok(Some(ShieldedData {
first: Right(shielded_outputs.remove(0)),
rest_spends: shielded_spends,
rest_outputs: shielded_outputs,
binding_sig: reader.read_64_bytes()?.into(),
}))
} else {
Ok(None)
}
}

impl ZcashSerialize for Transaction {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
// Post-Sapling, transaction size is limited to MAX_BLOCK_BYTES.
Expand Down Expand Up @@ -131,7 +190,7 @@ impl ZcashSerialize for Transaction {
outputs,
lock_time,
expiry_height,
value_balance,
sapling_value_balance,
shielded_data,
joinsplit_data,
} => {
Expand All @@ -142,7 +201,7 @@ impl ZcashSerialize for Transaction {
outputs.zcash_serialize(&mut writer)?;
lock_time.zcash_serialize(&mut writer)?;
writer.write_u32::<LittleEndian>(expiry_height.0)?;
value_balance.zcash_serialize(&mut writer)?;
sapling_value_balance.zcash_serialize(&mut writer)?;

// The previous match arms serialize in one go, because the
// internal structure happens to nicely line up with the
Expand All @@ -152,29 +211,19 @@ impl ZcashSerialize for Transaction {
// instead we have to interleave serialization of the
// ShieldedData and the JoinSplitData.

match shielded_data {
None => {
// Signal no shielded spends and no shielded outputs.
writer.write_compactsize(0)?;
writer.write_compactsize(0)?;
}
Some(shielded_data) => {
writer.write_compactsize(shielded_data.spends().count() as u64)?;
for spend in shielded_data.spends() {
spend.zcash_serialize(&mut writer)?;
}
writer.write_compactsize(shielded_data.outputs().count() as u64)?;
for output in shielded_data.outputs() {
output.zcash_serialize(&mut writer)?;
}
}
}
// Serialize ShieldedData without doing the binding_sig.
let sig_shielded_data = SigShieldedData {
sig: false,
shielded_data,
};
sig_shielded_data.zcash_serialize(&mut writer)?;

match joinsplit_data {
None => writer.write_compactsize(0)?,
Some(jsd) => jsd.zcash_serialize(&mut writer)?,
}

// Manually write the binding_sig after the JoinSplitData.
match shielded_data {
Some(sd) => writer.write_all(&<[u8; 64]>::from(sd.binding_sig)[..])?,
None => {}
Expand All @@ -185,6 +234,8 @@ impl ZcashSerialize for Transaction {
expiry_height,
inputs,
outputs,
shielded_data,
sapling_value_balance,
rest,
} => {
// Write version 5 and set the fOverwintered bit.
Expand All @@ -194,6 +245,20 @@ impl ZcashSerialize for Transaction {
writer.write_u32::<LittleEndian>(expiry_height.0)?;
inputs.zcash_serialize(&mut writer)?;
outputs.zcash_serialize(&mut writer)?;

// Version 5 internal structure is different from version 4.
// Here we can do the binding signature without interleave
// the serialization.

// Serialize the ShieldedData including the binding_sig.
let sig_shielded_data = SigShieldedData {
sig: true,
shielded_data,
};
sig_shielded_data.zcash_serialize(&mut writer)?;

sapling_value_balance.zcash_serialize(&mut writer)?;

// write the rest
writer.write_all(rest)?;
}
Expand Down Expand Up @@ -266,36 +331,19 @@ impl ZcashDeserialize for Transaction {
let outputs = Vec::zcash_deserialize(&mut reader)?;
let lock_time = LockTime::zcash_deserialize(&mut reader)?;
let expiry_height = block::Height(reader.read_u32::<LittleEndian>()?);
let value_balance = (&mut reader).zcash_deserialize_into()?;
let mut shielded_spends = Vec::zcash_deserialize(&mut reader)?;
let mut shielded_outputs = Vec::zcash_deserialize(&mut reader)?;
let sapling_value_balance = (&mut reader).zcash_deserialize_into()?;
let shielded_spends = Vec::zcash_deserialize(&mut reader)?;
let shielded_outputs = Vec::zcash_deserialize(&mut reader)?;
let joinsplit_data = OptV4Jsd::zcash_deserialize(&mut reader)?;

use futures::future::Either::*;
let shielded_data = if !shielded_spends.is_empty() {
Some(ShieldedData {
first: Left(shielded_spends.remove(0)),
rest_spends: shielded_spends,
rest_outputs: shielded_outputs,
binding_sig: reader.read_64_bytes()?.into(),
})
} else if !shielded_outputs.is_empty() {
Some(ShieldedData {
first: Right(shielded_outputs.remove(0)),
rest_spends: shielded_spends,
rest_outputs: shielded_outputs,
binding_sig: reader.read_64_bytes()?.into(),
})
} else {
None
};
let shielded_data =
Copy link
Contributor

Choose a reason for hiding this comment

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

Transaction::v4 isn't changing so I don't think we need to change this.

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 idea is to use the same code in V4 and V5 as they are doing almost the same for sapling stuff(except for the order of the fields).

Copy link
Contributor

Choose a reason for hiding this comment

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

Even though they have the same names, the underlying types of these fields are different, so it might be best to just duplicate some code.

deserialize_shielded_data(&mut reader, shielded_spends, shielded_outputs)?;

Ok(Transaction::V4 {
inputs,
outputs,
lock_time,
expiry_height,
value_balance,
sapling_value_balance,
shielded_data,
joinsplit_data,
})
Expand All @@ -309,6 +357,12 @@ impl ZcashDeserialize for Transaction {
let expiry_height = block::Height(reader.read_u32::<LittleEndian>()?);
let inputs = Vec::zcash_deserialize(&mut reader)?;
let outputs = Vec::zcash_deserialize(&mut reader)?;
let shielded_spends = Vec::zcash_deserialize(&mut reader)?;
let shielded_outputs = Vec::zcash_deserialize(&mut reader)?;
let shielded_data =
deserialize_shielded_data(&mut reader, shielded_spends, shielded_outputs)?;
let sapling_value_balance = (&mut reader).zcash_deserialize_into()?;
Comment on lines +360 to +364
Copy link
Contributor

Choose a reason for hiding this comment

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

If inputs and outputs are None, these fields do not exist in V5:
#1829 (comment)

I will add a test for this bug to the design in #1863.

After #1863 is completed, please revise all the code in this PR, based on the latest spec.

Screen Shot 2021-03-17 at 11 07 55


let mut rest = Vec::new();
reader.read_to_end(&mut rest)?;

Expand All @@ -317,6 +371,8 @@ impl ZcashDeserialize for Transaction {
expiry_height,
inputs,
outputs,
shielded_data,
sapling_value_balance,
rest,
})
}
Expand Down
5 changes: 3 additions & 2 deletions zebra-chain/src/transaction/shielded_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ impl ShieldedData {
/// https://zips.z.cash/protocol/protocol.pdf#saplingbalance
pub fn binding_verification_key(
&self,
value_balance: Amount,
sapling_value_balance: Amount,
) -> redjubjub::VerificationKeyBytes<Binding> {
let cv_old: ValueCommitment = self.spends().map(|spend| spend.cv).sum();
let cv_new: ValueCommitment = self.outputs().map(|output| output.cv).sum();
let cv_balance: ValueCommitment = ValueCommitment::new(jubjub::Fr::zero(), value_balance);
let cv_balance: ValueCommitment =
ValueCommitment::new(jubjub::Fr::zero(), sapling_value_balance);

let key_bytes: [u8; 32] = (cv_old - cv_new - cv_balance).into();

Expand Down
9 changes: 6 additions & 3 deletions zebra-chain/src/transaction/sighash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,13 +486,16 @@ impl<'a> SigHasher<'a> {
fn hash_value_balance<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
use Transaction::*;

let value_balance = match self.trans {
V4 { value_balance, .. } => value_balance,
let sapling_value_balance = match self.trans {
V4 {
sapling_value_balance,
..
} => sapling_value_balance,
V5 { .. } => unimplemented!("v5 transaction hash as specified in ZIP-225 and ZIP-244"),
V1 { .. } | V2 { .. } | V3 { .. } => unreachable!(ZIP243_EXPLANATION),
};

writer.write_all(&value_balance.to_bytes())?;
writer.write_all(&sapling_value_balance.to_bytes())?;

Ok(())
}
Expand Down
6 changes: 3 additions & 3 deletions zebra-consensus/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ where
// outputs,
// lock_time,
// expiry_height,
value_balance,
sapling_value_balance,
joinsplit_data,
shielded_data,
..
Expand Down Expand Up @@ -194,7 +194,7 @@ where
}

if let Some(shielded_data) = shielded_data {
check::shielded_balances_match(&shielded_data, *value_balance)?;
check::shielded_balances_match(&shielded_data, *sapling_value_balance)?;
for spend in shielded_data.spends() {
// TODO: check that spend.cv and spend.rk are NOT of small
// order.
Expand Down Expand Up @@ -238,7 +238,7 @@ where
// transaction to verify.
});

let bvk = shielded_data.binding_verification_key(*value_balance);
let bvk = shielded_data.binding_verification_key(*sapling_value_balance);
let _rsp = redjubjub_verifier
.ready_and()
.await?
Expand Down
4 changes: 2 additions & 2 deletions zebra-consensus/src/transaction/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError>
/// https://zips.z.cash/protocol/protocol.pdf#consensusfrombitcoin
pub fn shielded_balances_match(
shielded_data: &ShieldedData,
value_balance: Amount,
sapling_value_balance: Amount,
) -> Result<(), TransactionError> {
if (shielded_data.spends().count() + shielded_data.outputs().count() != 0)
|| i64::from(value_balance) == 0
|| i64::from(sapling_value_balance) == 0
{
Ok(())
} else {
Expand Down