Skip to content
Merged
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
200 changes: 200 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/Zip317Dimensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { WasmZip317Dimensions } from "../wasm/wasm_utxo.js";
import type { BitGoPsbt } from "./BitGoPsbt.js";
import type { CoinName } from "../coinName.js";
import {
hasOrchardReceiver,
toShieldedReceiverWithCoin,
toTransparentReceiverWithCoin,
} from "../zcashAddress.js";

/** The transparent input script types a Zcash wallet can spend. */
export const Zip317InputScriptType = ["p2sh", "p2shP2pk"] as const;
export type Zip317InputScriptType = (typeof Zip317InputScriptType)[number];

/**
* Options for {@link Zip317Dimensions.fromOutput}.
*/
export type Zip317FromOutputOptions = {
/**
* Set when `address` may be a ZIP-316 Unified Address whose Orchard/Ironwood receiver should be
* paid as a shielded output (one Ironwood output). Resolved like `ZcashDimensions.fromOutput`
* (via `zcashAddress.toShieldedReceiverWithCoin`): a Unified Address without such a receiver,
* or a malformed one, throws; an ordinary transparent address is sized as a transparent
* output. When unset, the address's transparent receiver is sized as a transparent output.
*/
isShielded?: boolean;
};

/**
* ZIP-317 dimensions for Zcash conventional fee estimation.
*
* Tracks what ZIP-317 prices — the transparent inputs' and outputs' serialized sizes and the
* Ironwood spends/outputs — rather than bitcoin weight. This is independent of `Dimensions` /
* `ZcashDimensions`: callers pick the estimator they want (`vsize × feeRate` from those, or
* {@link Zip317Dimensions.getConventionalFee} from this).
*
* Ironwood spends and outputs are tracked separately because bundles are unpadded: the action
* count is `max(spends, outputs)`, not their sum.
*
* This is a thin wrapper over the WASM implementation.
*/
export class Zip317Dimensions {
private constructor(private _wasm: WasmZip317Dimensions) {}

/**
* Create empty dimensions
*/
static empty(): Zip317Dimensions {
return new Zip317Dimensions(WasmZip317Dimensions.empty());
}

/**
* Create dimensions from a Zcash BitGoPsbt (any version)
*
* Transparent inputs are sized with the scriptSig they will finalize to (the real one once
* finalized), transparent outputs exactly, and the Ironwood side from the real (non-dummy)
* spends and outputs of the stored PCZT. After `combineProof` has dropped the PCZT, the counts
* recorded at extraction are used, so a finalized PSBT still reports its actions and fee.
*
* @throws WasmUtxoError `Zip317Error.NotZcashPsbt` for a non-Zcash PSBT, or
* `Zip317Error.IronwoodPczt` if the stored PCZT cannot be read
*/
static fromPsbt(psbt: BitGoPsbt): Zip317Dimensions {
return new Zip317Dimensions(WasmZip317Dimensions.from_psbt(psbt.wasm));
}

/**
* Create dimensions for a single transparent input
*
* @param params - Either `{ chain }` (Zcash wallets only have the P2SH chains 0 and 1) or
* `{ scriptType }`
*/
static fromInput(
params: { chain: number } | { scriptType: Zip317InputScriptType },
): Zip317Dimensions {
if ("scriptType" in params) {
return new Zip317Dimensions(WasmZip317Dimensions.from_input_script_type(params.scriptType));
}
return new Zip317Dimensions(WasmZip317Dimensions.from_input(params.chain));
}

/**
* Create dimensions for a single transparent output from script bytes
*/
static fromOutput(script: Uint8Array): Zip317Dimensions;
/**
* Create dimensions for a single output from a Zcash address for `network`.
*
* With `{ isShielded: true }`, a Unified Address is one Ironwood output (it must carry an
* Orchard/Ironwood receiver, else this throws) and a transparent address is a transparent
* output. Otherwise the address's transparent receiver is sized.
*/
static fromOutput(
address: string,
network: CoinName,
options?: Zip317FromOutputOptions,
): Zip317Dimensions;
/**
* Create dimensions for a single transparent output from script length only
*/
static fromOutput(params: { length: number }): Zip317Dimensions;
static fromOutput(
params: Uint8Array | string | { length: number },
network?: CoinName,
options?: Zip317FromOutputOptions,
): Zip317Dimensions {
if (typeof params === "string") {
if (network === undefined) {
throw new Error("network is required when passing an address string");
}
if (options?.isShielded && hasOrchardReceiver(params, network)) {
return Zip317Dimensions.fromIronwoodOutputs(1);
}
// For `isShielded`, this throws for a Unified Address without an Orchard/Ironwood receiver
// (or a malformed one) and returns a transparent address's scriptPubKey.
const script = options?.isShielded
? toShieldedReceiverWithCoin(params, network)
: toTransparentReceiverWithCoin(params, network);
return new Zip317Dimensions(WasmZip317Dimensions.from_output_script_length(script.length));
}
// Both Uint8Array and { length: number } have .length
return new Zip317Dimensions(WasmZip317Dimensions.from_output_script_length(params.length));
}

/**
* Create dimensions for `count` Ironwood spends (shielded notes being spent)
*/
static fromIronwoodSpends(count: number): Zip317Dimensions {
return new Zip317Dimensions(WasmZip317Dimensions.from_ironwood_spends(count));
}

/**
* Create dimensions for `count` Ironwood (shielded) outputs
*/
static fromIronwoodOutputs(count: number): Zip317Dimensions {
return new Zip317Dimensions(WasmZip317Dimensions.from_ironwood_outputs(count));
}

/**
* Combine with another Zip317Dimensions instance
*/
plus(other: Zip317Dimensions): Zip317Dimensions {
return new Zip317Dimensions(this._wasm.plus(other._wasm));
}

/**
* Multiply dimensions by a scalar
*/
times(n: number): Zip317Dimensions {
return new Zip317Dimensions(this._wasm.times(n));
}

/**
* The sum of the transparent inputs' estimated final serialized sizes, in bytes
*/
get txInTotalSize(): number {
return this._wasm.get_tx_in_total_size();
}

/**
* The sum of the transparent outputs' serialized sizes, in bytes
*/
get txOutTotalSize(): number {
return this._wasm.get_tx_out_total_size();
}

/**
* The number of Ironwood spends
*/
get ironwoodSpends(): number {
return this._wasm.get_ironwood_spends();
}

/**
* The number of Ironwood outputs
*/
get ironwoodOutputs(): number {
return this._wasm.get_ironwood_outputs();
}

/**
* The number of Ironwood actions: `max(spends, outputs)`
*/
get ironwoodActions(): number {
return this._wasm.get_ironwood_actions();
}

/**
* The ZIP-317 `logical_actions`
*/
getLogicalActions(): number {
return this._wasm.get_logical_actions();
}

/**
* The ZIP-317 conventional fee in zatoshi: `5_000 × max(2, logical_actions)`
*/
getConventionalFee(): bigint {
return this._wasm.get_conventional_fee();
}
}
5 changes: 5 additions & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export { ECPair } from "./ecpair.js";
export { BIP32 } from "./bip32.js";
export { Dimensions } from "./fixedScriptWallet/Dimensions.js";
export { ZcashDimensions } from "./fixedScriptWallet/ZcashDimensions.js";
export {
Zip317Dimensions,
Zip317InputScriptType,
type Zip317FromOutputOptions,
} from "./fixedScriptWallet/Zip317Dimensions.js";
export {
ZcashBitGoPsbt,
ZcashPsbt,
Expand Down
8 changes: 8 additions & 0 deletions packages/wasm-utxo/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum WasmUtxoError {
UnifiedAddress(crate::zcash::unified_address::UnifiedAddressError),
ZcashV6(crate::zcash::v6::ZcashV6Error),
Ironwood(crate::zcash::ironwood_build::IronwoodBuildError),
Zip317(crate::zcash::zip317::Zip317Error),
VerifyV6Signature(VerifyV6SignatureError),
}

Expand All @@ -40,6 +41,7 @@ impl fmt::Display for WasmUtxoError {
WasmUtxoError::UnifiedAddress(e) => write!(f, "{}", e),
WasmUtxoError::ZcashV6(e) => write!(f, "{}", e),
WasmUtxoError::Ironwood(e) => write!(f, "{}", e),
WasmUtxoError::Zip317(e) => write!(f, "{}", e),
WasmUtxoError::VerifyV6Signature(e) => write!(f, "{}", e),
}
}
Expand All @@ -53,6 +55,7 @@ impl WasmErrorCode for WasmUtxoError {
WasmUtxoError::UnifiedAddress(e) => e.code(),
WasmUtxoError::ZcashV6(e) => e.code(),
WasmUtxoError::Ironwood(e) => e.code(),
WasmUtxoError::Zip317(e) => e.code(),
WasmUtxoError::VerifyV6Signature(e) => e.code(),
}
}
Expand Down Expand Up @@ -111,6 +114,11 @@ impl From<crate::zcash::ironwood_build::IronwoodBuildError> for WasmUtxoError {
WasmUtxoError::Ironwood(err)
}
}
impl From<crate::zcash::zip317::Zip317Error> for WasmUtxoError {
fn from(err: crate::zcash::zip317::Zip317Error) -> Self {
WasmUtxoError::Zip317(err)
}
}
impl From<VerifyV6SignatureError> for WasmUtxoError {
fn from(err: VerifyV6SignatureError) -> Self {
WasmUtxoError::VerifyV6Signature(err)
Expand Down
27 changes: 27 additions & 0 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ pub enum ZecV6KeySubtype {
/// from the scriptPubKey) — kept so output parsing can hand back the exact UA the caller
/// passed, not one reconstructed as a bare transparent address.
TransparentUnifiedAddress = 0x06,
/// The real (non-dummy) Ironwood spend and output counts of the PCZT dropped by
/// [`take_ironwood_pczt`], as two little-endian `u32`s. Written alongside `IronwoodExtracted`
/// so ZIP-317 fee estimation still sees the bundle's actions once the PCZT itself is gone.
IronwoodExtractedCounts = 0x07,
}

fn set_zec_v6(
Expand Down Expand Up @@ -386,6 +390,29 @@ pub fn is_ironwood_extracted(psbt: &miniscript::bitcoin::psbt::Psbt) -> bool {
get_zec_v6(psbt, ZecV6KeySubtype::IronwoodExtracted).is_some()
}

/// Record the real Ironwood spend/output counts of a PCZT about to be dropped by
/// [`take_ironwood_pczt`] (see [`ZecV6KeySubtype::IronwoodExtractedCounts`]).
pub fn set_ironwood_extracted_counts(
psbt: &mut miniscript::bitcoin::psbt::Psbt,
spends: u32,
outputs: u32,
) {
let mut value = spends.to_le_bytes().to_vec();
value.extend_from_slice(&outputs.to_le_bytes());
set_zec_v6(psbt, ZecV6KeySubtype::IronwoodExtractedCounts, value);
}

/// The `(spends, outputs)` recorded by [`set_ironwood_extracted_counts`], or `None` if absent or
/// malformed.
pub fn get_ironwood_extracted_counts(psbt: &miniscript::bitcoin::psbt::Psbt) -> Option<(u32, u32)> {
let value = get_zec_v6(psbt, ZecV6KeySubtype::IronwoodExtractedCounts)?;
let (spends, outputs) = value.as_slice().split_at_checked(4)?;
Some((
u32::from_le_bytes(spends.try_into().ok()?),
u32::from_le_bytes(outputs.try_into().ok()?),
))
}

/// Store the Zcash v6 (Ironwood) header params — `version_group_id` and `expiry_height` — under the
/// `BITGO_ZEC_V6` namespace. `version_group_id`'s presence marks the PSBT as v6; `expiry_height` is
/// always written too (even when 0, a valid "no expiry" value, so it can be told apart from absent).
Expand Down
Loading
Loading