Block header parsing and transaction ID computation#66
Conversation
ebfull
left a comment
There was a problem hiding this comment.
Just fun complaints, nothing urgent. Nice PR!
There was a problem hiding this comment.
We're bringing in two different versions of this crate apparently.
There was a problem hiding this comment.
The older version is coming in through the aes crate, which is used via the fpe by zip32. We can bump fpe to the latest aes crate, which will resolve this discrepancy.
There was a problem hiding this comment.
I've opened str4d/fpe#9 with this change, along with some minor cleanups and improvements. I've confirmed that it reduces the number of dependencies by three (two of which were older versions of crates we still depend on).
There was a problem hiding this comment.
There was a problem hiding this comment.
Instead, you could initialize a local clone of the self.0: [u8; 32] and then [T]::reverse() on that, rather than allocating.
(Not blocking.)
There was a problem hiding this comment.
Same as the other review comment about something similar to this.
|
Rebased the last few commits again because I forgot to correct the Cargo.lock after rebasing. |
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
| pub struct BlockHash(pub [u8; 32]); | ||
|
|
||
| impl fmt::Display for BlockHash { |
There was a problem hiding this comment.
Aside from the name, BlockHash is the same as TxId and in this commit we write the same fn fmt twice. This makes me wonder if there should be a single type (named ByteArray32 or something like that) rather than two.
There was a problem hiding this comment.
I'm not convinced that the extra indirection to the underlying bytes is worth it yet. If we find the reverse-hex format is bleeding in too much from zcashd then we can revisit this.
|
|
||
| impl Transaction { | ||
| fn from_data(data: TransactionData) -> io::Result<Self> { | ||
| let mut tx = Transaction(data, TxId([0; 32])); |
There was a problem hiding this comment.
It feels we are going back and forth a bit having to construct the transaction and write it's data to an array before we can set the txid. It would be possible to do this all at once, but maybe it's not worth the trouble.
There was a problem hiding this comment.
By design, I chose to only allow serialization of complete transactions (so that the transaction format is not used for storing "partially-complete transactions", which greatly complicates the job of the transaction parser). Thus it is necessary either to do what we do here, or to have the transaction serializer be a standalone function that takes all the transaction components as inputs. I don't think the complexity of the latter is worth it; the txid field of Transaction is private, so we know that only this module (and submodules) will be able to see and/or modify it directly.
There was a problem hiding this comment.
This could be done by moving the write<W: Write>(&self, mut writer: W) function from Transaction to TransactionData, but I could see this being beyond the scope of this PR and we can revisit it later.
| pub version: i32, | ||
| pub prev_block: BlockHash, | ||
| pub merkle_root: [u8; 32], | ||
| pub final_sapling_root: [u8; 32], |
There was a problem hiding this comment.
Not sure if this is out of scope for the PR, but the BlockHash is a typed wrapper for a [u8; 32]; are the merkle_root and final_sapling_root comparable 32-byte arrays, or semantically-different types? Should they also be newtype wrappers?
There was a problem hiding this comment.
They are all hash outputs, but of at least two different hash functions. In zcashd they are represented by the same 256-bit binary blob type, which has the "feature" that when rendered in hex in zcashd RPC output, the bytes are reversed (behaviour inherited from Bitcoin Core).
I use separate types for block hashes and transaction IDs because those are the only two that external code is likely to actually interact with, whereas other block header fields (thus far) will only be used internally. Once the refactor is further along and we get to the stage of cleaning up the crate APIs, we should revisit this.
| reader.read_exact(&mut merkle_root)?; | ||
|
|
||
| let mut final_sapling_root = [0; 32]; | ||
| reader.read_exact(&mut final_sapling_root)?; |
There was a problem hiding this comment.
I don't think there's anything wrong with this code, but I get a little anxious about the let mut bytes = zeros; read bytes; pattern, since (considering the reading, not the zeroing, as the "real" initialization) the variable initialization is unlinked from its declaration, and the resulting variable is left mutable even if it doesn't need to be.
Since reading 32 bytes is a pretty common idiom, maybe it would make sense to define some extension behaviour like so:
pub trait ReadExt {
fn read_32_bytes(&mut self) -> io::Result<[u8; 32]>;
}
impl<R: Read> ReadExt for R {
#[inline]
fn read_32_bytes(&mut self) -> io::Result<[u8; 32]> {
let mut bytes = [0; 32];
self.read_exact(&mut bytes)?;
Ok(bytes)
}
}so that this byte-handling code can become, e.g.,
let prev_block = BlockHash(reader.read_32_bytes()?);
let merkle_root = reader.read_32_bytes()?;
let final_sapling_root = reader.read_32_bytes()?;There was a problem hiding this comment.
This is a commonly-used idiom throughout the existing codebase, so we should address this comprehensively. Opened #72
Eirik0
left a comment
There was a problem hiding this comment.
utACK. My comments are non-blocking.
…cash#66) Replace generic `pow_vartime([5])` with `x.square().square() * x` (3 muls instead of a variable-time exponentiation loop). This is called 80 times per Poseidon hash (8 full rounds * 3 + 56 partial rounds), so the savings compound across millions of tree hashes. Benchmark (100k nullifiers): 312ms -> 245ms (-21.7%) Also adds a criterion benchmark for tree construction. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
No description provided.