-
Notifications
You must be signed in to change notification settings - Fork 117
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
value_balance: Amount, | ||
/// The rest of the transaction as bytes | ||
rest: Vec<u8>, | ||
}, | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()), | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it looks like we can't use this type, because |
||||||||
|
||||||||
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. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
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. | ||||||||
|
@@ -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 => {} | ||||||||
|
@@ -185,6 +234,8 @@ impl ZcashSerialize for Transaction { | |||||||
expiry_height, | ||||||||
inputs, | ||||||||
outputs, | ||||||||
shielded_data, | ||||||||
value_balance, | ||||||||
rest, | ||||||||
} => { | ||||||||
// Write version 5 and set the fOverwintered bit. | ||||||||
|
@@ -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)?; | ||||||||
|
||||||||
value_balance.zcash_serialize(&mut writer)?; | ||||||||
|
||||||||
// write the rest | ||||||||
writer.write_all(rest)?; | ||||||||
} | ||||||||
|
@@ -267,28 +332,11 @@ impl ZcashDeserialize for Transaction { | |||||||
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 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 = | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||||
|
@@ -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 value_balance = (&mut reader).zcash_deserialize_into()?; | ||||||||
|
||||||||
let mut rest = Vec::new(); | ||||||||
reader.read_to_end(&mut rest)?; | ||||||||
|
||||||||
|
@@ -317,6 +371,8 @@ impl ZcashDeserialize for Transaction { | |||||||
expiry_height, | ||||||||
inputs, | ||||||||
outputs, | ||||||||
shielded_data, | ||||||||
value_balance, | ||||||||
rest, | ||||||||
}) | ||||||||
} | ||||||||
|
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.
v5 transactions also include the Orchard value balance.
So we'll need to rename
shielded_data
,ShieldedData
, andvalue_balance
to includesapling
. We should make this change acrossV4
andV5
transactions, for consistency.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.
Actually, we'll need to make this change in this PR, because the v5 transaction format changes the fields inside
ShieldedData
. So we'll need to:ShieldedData
toSaplingV4ShieldedData
SaplingV5ShieldedData
OrchardActionData
@dconnolly what's a good name for the new
SaplingV5ShieldedData
type?It doesn't have any proofs or signatures, just value commitments, nullifiers, keys, and ciphertext.
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.
What about having
ShieldedData::V4
andShieldedData::V5
instead ?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.
That's still easily confused with Orchard shielded data. (And a bit confusing with Sprout shielded data as well.)