Skip to content
Draft
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
11 changes: 5 additions & 6 deletions modules/utxo-lib/src/bitgo/zcash/ZcashBufferutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,10 @@ export function fromBufferV4<TNumber extends number | bigint>(
}

if (tx.isSaplingCompatible()) {
const valueBalance = bufferReader.readSlice(8);
if (!valueBalance.equals(VALUE_INT64_ZERO)) {
/* istanbul ignore next */
throw new UnsupportedTransactionError(`valueBalance must be zero`);
}
// valueBalance is a signed int64; negative for t->z shielding transactions.
// Store raw bytes so toBuffer/getId can round-trip the tx without mutation.
// https://github.com/zcash/zcash/blob/v4.5.1/src/primitives/transaction.h#L283
tx.saplingValueBalance = bufferReader.readSlice(8);

// https://github.com/zcash/zcash/blob/v4.5.1/src/primitives/transaction.h#L863
readEmptySaplingBundle(bufferReader);
Expand Down Expand Up @@ -167,7 +166,7 @@ export function toBufferV4<TNumber extends number | bigint>(
}

if (tx.isSaplingCompatible()) {
bufferWriter.writeSlice(VALUE_INT64_ZERO);
bufferWriter.writeSlice(tx.saplingValueBalance ?? VALUE_INT64_ZERO);
bufferWriter.writeVarInt(0); // vShieldedSpendLength
bufferWriter.writeVarInt(0); // vShieldedOutputLength
}
Expand Down
7 changes: 6 additions & 1 deletion modules/utxo-lib/src/bitgo/zcash/ZcashTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ export class ZcashTransaction<TNumber extends number | bigint = number> extends
// Block height after which this transactions will expire, or 0 to disable expiry
expiryHeight = 0;
consensusBranchId: number;
// Raw 8-byte little-endian signed int64 valueBalance field for Sapling v4 txs.
// Negative when value flows from the transparent pool into the shielded pool
// (t->z shielding). Stored verbatim so toBuffer/getId round-trip correctly.
saplingValueBalance: Buffer = VALUE_INT64_ZERO;

constructor(public network: ZcashNetwork, tx?: ZcashTransaction<bigint | number>, amountType?: 'bigint' | 'number') {
super(network, tx, amountType);
Expand All @@ -123,6 +127,7 @@ export class ZcashTransaction<TNumber extends number | bigint = number> extends
this.overwintered = tx.overwintered;
this.versionGroupId = tx.versionGroupId;
this.expiryHeight = tx.expiryHeight;
this.saplingValueBalance = tx.saplingValueBalance;

if (tx.consensusBranchId !== undefined) {
consensusBranchId = tx.consensusBranchId;
Expand Down Expand Up @@ -378,7 +383,7 @@ export class ZcashTransaction<TNumber extends number | bigint = number> extends
bufferWriter.writeUInt32(this.locktime);
bufferWriter.writeUInt32(this.expiryHeight);
if (this.isSaplingCompatible()) {
bufferWriter.writeSlice(VALUE_INT64_ZERO);
bufferWriter.writeSlice(this.saplingValueBalance);
}
bufferWriter.writeInt32(hashType);

Expand Down
43 changes: 43 additions & 0 deletions modules/utxo-lib/test/bitgo/zcash/ZcashTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as assert from 'assert';
import { networks } from '../../../src';
import { ZcashTransaction } from '../../../src/bitgo';

// Minimal Sapling v4 transaction with valueBalance = -5 (t->z shielding).
// Built as: header(0x80000004) | versionGroupId(0x892F2085) | vin(0) | vout(0) |
// locktime(0) | expiryHeight(0) | valueBalance(-5 as int64le) |
// vSpendsSapling(0) | vOutputsSapling(0) | vJoinSplit(0)
const SHIELDING_TX_HEX = '0400008085202f8900000000000000000000fbffffffffffffff000000';

describe('ZcashTransaction (Sapling valueBalance)', function () {
describe('round-trip for t->z shielding tx (negative valueBalance)', function () {
it('parses without throwing', function () {
const tx = ZcashTransaction.fromBuffer(Buffer.from(SHIELDING_TX_HEX, 'hex'), false, 'number', networks.zcash);
// saplingValueBalance stores raw bytes; -5 in int64le is fb ff ff ff ff ff ff ff
assert.strictEqual(tx.saplingValueBalance.toString('hex'), 'fbffffffffffffff');
});

it('re-serializes to the same hex (toBuffer round-trip)', function () {
const tx = ZcashTransaction.fromBuffer(Buffer.from(SHIELDING_TX_HEX, 'hex'), false, 'number', networks.zcash);
assert.strictEqual(tx.toBuffer().toString('hex'), SHIELDING_TX_HEX);
});

it('computes getId() without throwing', function () {
const tx = ZcashTransaction.fromBuffer(Buffer.from(SHIELDING_TX_HEX, 'hex'), false, 'number', networks.zcash);
// Just verify it does not throw; the exact txid is deterministic from the hex above.
assert.doesNotThrow(() => tx.getId());
});

it('clones correctly', function () {
const tx = ZcashTransaction.fromBuffer(Buffer.from(SHIELDING_TX_HEX, 'hex'), false, 'number', networks.zcash);
const cloned = tx.clone();
assert.strictEqual(cloned.toBuffer().toString('hex'), SHIELDING_TX_HEX);
});
});

describe('round-trip for transparent tx (zero valueBalance)', function () {
it('saplingValueBalance defaults to all-zero bytes', function () {
const tx = new ZcashTransaction(networks.zcash);
assert.strictEqual(tx.saplingValueBalance.toString('hex'), '0000000000000000');
});
});
});
Loading