-
Notifications
You must be signed in to change notification settings - Fork 3
feat(zcash): add support for v6 transaction format #327
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
base: zcash-v6-transaction
Are you sure you want to change the base?
Changes from all commits
eb2cd89
f4260fb
3743ca1
aefce59
f3498d2
716adb6
375adde
1284cca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,11 @@ | ||
| import { BitGoPsbt as WasmBitGoPsbt, zcash_branch_id_for_height } from "../wasm/wasm_utxo.js"; | ||
| import { | ||
| BitGoPsbt as WasmBitGoPsbt, | ||
| zcash_branch_id_for_height, | ||
| zcash_compute_v6_txid, | ||
| zcash_is_address_component_of, | ||
| zcash_parse_unified_address, | ||
| zcash_resolve_unified_address_component, | ||
| } from "../wasm/wasm_utxo.js"; | ||
| import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js"; | ||
| import { BitGoPsbt, type CreateEmptyOptions, type HydrationUnspent } from "./BitGoPsbt.js"; | ||
| import { ZcashTransaction, type ITransaction } from "../transaction.js"; | ||
|
|
@@ -280,6 +287,75 @@ export class ZcashBitGoPsbt extends BitGoPsbt { | |
| return zcash_branch_id_for_height(network, height); | ||
| } | ||
|
|
||
| /** | ||
| * Compute the ZIP-244 txid of a Zcash v6 (Ironwood/NU6.3) transaction. | ||
| * | ||
| * @param txBytes - Raw v6 transaction bytes | ||
| * @returns The 32-byte txid in internal byte order (reverse for display) | ||
| * @throws If the bytes are not a valid v6 transaction | ||
| */ | ||
| static computeV6Txid(txBytes: Uint8Array): Uint8Array { | ||
| return zcash_compute_v6_txid(txBytes); | ||
| } | ||
|
|
||
| /** | ||
| * Parse a ZIP-316 Unified Address and return the requested receiver bytes. | ||
| * | ||
| * Ironwood reuses the existing Orchard receiver (typecode 0x03). | ||
| * | ||
| * @param address - The Bech32m unified address string | ||
| * @param network - Zcash network name ("zcash", "zcashTest", "zec", "tzec") | ||
| * @param ironwood - `true` → Orchard/Ironwood receiver (43 bytes: diversifier + | ||
| * pk_d); `false` → transparent receiver as scriptPubKey bytes (P2PKH/P2SH) | ||
| * @returns The receiver bytes | ||
| * @throws If the address is malformed or lacks a receiver of the requested type | ||
| */ | ||
| static parseUnifiedAddress( | ||
|
Contributor
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. Same "opaque bytes" concern as the Prefer a structured return: decode once into a typed Generated by Claude Code |
||
| address: string, | ||
| network: ZcashNetworkName, | ||
| ironwood: boolean, | ||
| ): Uint8Array { | ||
| return zcash_parse_unified_address(address, network, ironwood); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve a single component of a ZIP-316 Unified Address. | ||
| * | ||
| * @param address - The Bech32m unified address string | ||
| * @param network - Zcash network name ("zcash", "zcashTest", "zec", "tzec") | ||
| * @param resolveShieldedComponent - `true` → the shielded (Ironwood/Orchard) | ||
| * receiver (43 bytes: diversifier + pk_d); `false` → the transparent receiver | ||
| * as scriptPubKey bytes (P2PKH/P2SH) | ||
| * @returns The requested component's bytes | ||
| * @throws If the address is malformed or lacks a receiver of the requested type | ||
| */ | ||
| static resolveUnifiedAddressComponent( | ||
| address: string, | ||
| network: ZcashNetworkName, | ||
| resolveShieldedComponent: boolean, | ||
| ): Uint8Array { | ||
| return zcash_resolve_unified_address_component(address, network, resolveShieldedComponent); | ||
| } | ||
|
|
||
| /** | ||
| * Determine whether `candidate` is a component of the Unified Address `unified`. | ||
| * | ||
| * @param unified - A ZIP-316 Unified Address (the container) | ||
| * @param candidate - Either another Unified Address (matches if all of its | ||
| * receivers are contained in `unified`) or a transparent Zcash address (matches | ||
| * if `unified`'s transparent receiver equals it) | ||
| * @param network - Zcash network name; both addresses must belong to it | ||
| * @returns `true` if `candidate` is contained in `unified` | ||
| * @throws If either address is malformed or on the wrong network | ||
| */ | ||
| static isAddressComponentOf( | ||
| unified: string, | ||
| candidate: string, | ||
| network: ZcashNetworkName, | ||
| ): boolean { | ||
| return zcash_is_address_component_of(unified, candidate, network); | ||
| } | ||
|
|
||
| /** | ||
| * Extract the final Zcash transaction from a finalized PSBT | ||
| * | ||
|
|
||
|
Contributor
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. additions should go to zec-namespaced file, maybe take some zcash scoped funcs here with you as well |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| //! BLAKE2b-256 with personalization, as used by Zcash ZIP-243 (sighash) and | ||
| //! ZIP-244 (txid / v6 sighash) hash trees. | ||
| //! | ||
| //! Every digest in the ZIP-244 tree is a BLAKE2b-256 hash keyed by a 16-byte | ||
| //! personalization string. This mirrors the `blake2b_256_personal` helper in the | ||
| //! BitGo miniscript fork (which is `pub(crate)` there and therefore not reachable | ||
| //! from this crate), including the fix for block-aligned inputs. | ||
|
|
||
| use blake2::digest::core_api::{Buffer, UpdateCore, VariableOutputCore}; | ||
| use blake2::digest::Output; | ||
| use blake2::Blake2bVarCore; | ||
|
|
||
| /// Compute a BLAKE2b digest of `data` with a `personalization` string and an | ||
| /// explicit output length (1..=64 bytes). | ||
| /// | ||
| /// The output length is part of the BLAKE2b parameter block, so it changes the | ||
| /// digest — this is not truncation. Used by F4Jumble (ZIP-316), which needs both | ||
| /// 64-byte and shorter outputs. | ||
| /// | ||
| /// The `Buffer`/`digest_blocks` path retains the final block until finalization so | ||
| /// the correct finalization flag is used even when `data.len()` is an exact multiple | ||
| /// of the 128-byte BLAKE2b block size (the block-aligned bug fixed in the fork). | ||
| pub fn blake2b_var_personal(data: &[u8], personalization: &[u8], out_len: usize) -> Vec<u8> { | ||
| debug_assert!( | ||
| (1..=64).contains(&out_len), | ||
| "BLAKE2b output length out of range" | ||
| ); | ||
| let mut core = Blake2bVarCore::new_with_params(&[], personalization, 0, out_len); | ||
| let mut buffer: Buffer<Blake2bVarCore> = Default::default(); | ||
| buffer.digest_blocks(data, |blocks| core.update_blocks(blocks)); | ||
|
|
||
| let mut full_output: Output<Blake2bVarCore> = Default::default(); | ||
| VariableOutputCore::finalize_variable_core(&mut core, &mut buffer, &mut full_output); | ||
|
|
||
| full_output[..out_len].to_vec() | ||
| } | ||
|
|
||
| /// Compute a BLAKE2b-256 digest of `data` with a 16-byte `personalization` string. | ||
| /// | ||
| /// The personalization must be at most 16 bytes (ZIP personalization strings are | ||
| /// exactly 16 bytes, e.g. `b"ZTxIdIronwd_H_v6"`). | ||
| pub fn blake2b_256_personal(data: &[u8], personalization: &[u8]) -> [u8; 32] { | ||
| let out = blake2b_var_personal(data, personalization, 32); | ||
| let mut result = [0u8; 32]; | ||
| result.copy_from_slice(&out); | ||
| result | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn personalization_affects_output() { | ||
| let data = b"the quick brown fox"; | ||
| let a = blake2b_256_personal(data, b"ZTxIdPrevoutHash"); | ||
| let b = blake2b_256_personal(data, b"ZTxIdSequencHash"); | ||
| assert_ne!(a, b); | ||
| // Deterministic | ||
| assert_eq!(a, blake2b_256_personal(data, b"ZTxIdPrevoutHash")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn block_aligned_input_is_stable() { | ||
| // 256 bytes = exactly 2 BLAKE2b blocks — exercises the finalization-flag fix. | ||
| let data = vec![0xabu8; 256]; | ||
| let h = blake2b_256_personal(&data, b"ZTxIdOutputsHash"); | ||
| // Just assert it produces a non-trivial, deterministic 32-byte output. | ||
| assert_ne!(h, [0u8; 32]); | ||
| assert_eq!(h, blake2b_256_personal(&data, b"ZTxIdOutputsHash")); | ||
| } | ||
| } |
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.
why not a method on a psbt? why take txBytes?