aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-05 12:24:03 -0700
committerAdam Soltys <[email protected]>2026-08-05 12:24:03 -0700
commit68fd614f8cb404904e1939ab711f7c7ba4350c32 (patch)
tree861637d85964802e37211d98d536d35d796ca1bd /src
parent1307f8930283393fcf987cfa7e5cfa054676396a (diff)
downloadcoinosv3-68fd614f8cb404904e1939ab711f7c7ba4350c32.tar.gz
coinosv3-68fd614f8cb404904e1939ab711f7c7ba4350c32.zip
hal speaks pver 5: preimages prove their size, and fees round up as one
0.6.0 servers require the new hashlock clauses (OP_SIZE 32 guard) for Lightning and rounds. New policies/transitions carry the v1 scripts and type bytes; _v0 variants keep old vtxos reconstructing and exiting. The retired third-party HTLC policy is dropped (upstream took 0x08). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01M5kFodm9azZJVTQxnkDdKe
Diffstat (limited to 'src')
-rw-r--r--src/ark/board.js3
-rw-r--r--src/ark/manager.js9
-rw-r--r--src/ark/offboard.js7
-rw-r--r--src/ark/proto.js28
-rw-r--r--src/ark/refresh.js31
-rw-r--r--src/ark/send.js55
-rw-r--r--src/ark/validate.js5
7 files changed, 84 insertions, 54 deletions
diff --git a/src/ark/board.js b/src/ark/board.js
index f71c0b3..7544353 100644
--- a/src/ark/board.js
+++ b/src/ark/board.js
@@ -23,8 +23,9 @@ const u64le = (n) => {
// Fee fields are absent when zero (proto3) — e.g. Second's signet server
// boards for free — so every term defaults to 0.
+// pver >= 4 rounds the ppm component UP to the satoshi (server-side calculate()).
export const boardFee = (amountSat, fees = {}) =>
- Math.max(fees.minFeeSat || 0, (fees.baseFeeSat || 0) + Math.floor((amountSat * (fees.ppm || 0)) / 1_000_000));
+ Math.max(fees.minFeeSat || 0, (fees.baseFeeSat || 0) + Math.ceil((amountSat * (fees.ppm || 0)) / 1_000_000));
export const p2trAddress = (programXOnly, hrp = 'bcrt') =>
bech32m.encode(hrp, [1, ...bech32m.toWords(programXOnly)], 1023);
diff --git a/src/ark/manager.js b/src/ark/manager.js
index f9819a0..a478d19 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -1250,14 +1250,17 @@ export class ArkManager {
// ---- refresh (also the consolidation primitive) ----
refreshFee(inputs, tip) {
+ // pver >= 4: ppm accumulates sub-satoshi across ALL inputs and rounds UP
+ // once at the end (the server's calc_ppm_expiry_fee). Per-input floor —
+ // the old way — undercharges and the server rejects the round intent.
const table = this.info.refreshFees.ppmExpiryTable;
- let fee = this.info.refreshFees.baseFeeSat;
+ let ppmUnits = 0;
for (const v of inputs) {
const blocks = v.expiryHeight - tip;
const entry = table.filter((e) => e.thresholdBlocks <= blocks).pop();
- fee += Math.floor((v.amountSat * (entry?.ppm ?? 0)) / 1_000_000);
+ ppmUnits += v.amountSat * (entry?.ppm ?? 0);
}
- return fee;
+ return this.info.refreshFees.baseFeeSat + Math.ceil(ppmUnits / 1_000_000);
}
async refresh(vtxoIds) {
diff --git a/src/ark/offboard.js b/src/ark/offboard.js
index 5f10bde..ae6530d 100644
--- a/src/ark/offboard.js
+++ b/src/ark/offboard.js
@@ -63,13 +63,14 @@ export const feeRateKwu = (satVkb) => Math.floor(satVkb / 4);
export function offboardFee({ spkLen, satVkb, fees, tip, inputs }) {
const wu = (fees.fixedAdditionalVb + spkLen) * 4;
const weightFee = Math.floor((feeRateKwu(satVkb) * wu + 999) / 1000);
- let ppmFee = 0;
+ // pver >= 4: sub-satoshi ppm accumulates across vtxos, one round-up at the end
+ let ppmUnits = 0;
for (const v of inputs) {
const blocks = Math.max(0, v.expiryHeight - tip);
const entry = [...fees.ppmExpiryTable].reverse().find((e) => blocks >= e.thresholdBlocks);
- if (entry) ppmFee += Math.floor((v.amountSat * entry.ppm) / 1_000_000);
+ if (entry) ppmUnits += v.amountSat * entry.ppm;
}
- return fees.baseFeeSat + weightFee + ppmFee;
+ return fees.baseFeeSat + weightFee + Math.ceil(ppmUnits / 1_000_000);
}
// OffboardRequestAttestation: BIP340 signature over
diff --git a/src/ark/proto.js b/src/ark/proto.js
index 81651ee..a1f2fdc 100644
--- a/src/ark/proto.js
+++ b/src/ark/proto.js
@@ -74,7 +74,11 @@ export function* pbFields(buf) {
// ---------------------------------------------------------------------------
// Ark protocol version we speak (server advertises min/max in Handshake).
-export const PVER = 3;
+// 5 = hashlock clauses v1 (preimage size check in scripts, new policy type
+// bytes, HashLockedCosigned v1 transitions) — required by 0.6.0 servers for
+// Lightning and round participation. Implies pver-4 semantics too: ppm fees
+// round UP and expiry-ppm is charged on the total across VTXOs.
+export const PVER = 5;
export class GrpcError extends Error {
constructor(status, message, method) {
@@ -298,31 +302,39 @@ function decodeGenesisTransition(r) {
const tapTweak = hex.encode(r.bytes(32));
return { type: 'arkoor', clientCosigners, tapTweak, signature: optSignature(r) };
}
- if (type === 3) { // HashLockedCosigned
+ if (type === 3 || type === 4) {
+ // HashLockedCosigned: type 3 = v0 (pre-0.6.0 scripts), 4 = v1 (unlock
+ // clause checks the preimage is 32 bytes). Same wire shape; the type
+ // decides which script generation reconstructs/validates the leaf.
const userPubkey = hex.encode(r.bytes(33));
const signature = optSignature(r);
const tag = r.u8();
const unlock = { [tag === 0 ? 'preimage' : 'hash']: hex.encode(r.bytes(32)) };
- return { type: 'hashLockedCosigned', userPubkey, signature, unlock };
+ return { type: type === 4 ? 'hashLockedCosigned' : 'hashLockedCosigned_v0', userPubkey, signature, unlock };
}
throw new Error('unknown genesis transition type ' + type);
}
function decodePolicy(r) {
const type = r.u8();
+ // Bare names are the v1 (0.6.0) policies; _v0 marks the legacy scripts that
+ // existing vtxos may still carry. (The retired third-party HTLC proposal
+ // used 0x08, which upstream has since assigned to serverHtlcRecv v1.)
if (type === 0x00) return { type: 'pubkey', userPubkey: hex.encode(r.bytes(33)) };
if (type === 0x01) return {
- type: 'serverHtlcSend', userPubkey: hex.encode(r.bytes(33)),
+ type: 'serverHtlcSend_v0', userPubkey: hex.encode(r.bytes(33)),
paymentHash: hex.encode(r.bytes(32)), htlcExpiry: r.u32(),
};
if (type === 0x02) return {
- type: 'serverHtlcRecv', userPubkey: hex.encode(r.bytes(33)),
+ type: 'serverHtlcRecv_v0', userPubkey: hex.encode(r.bytes(33)),
paymentHash: hex.encode(r.bytes(32)), htlcExpiry: r.u32(), htlcExpiryDelta: r.u16(),
};
- // third-party HTLC (claim/refund between two users) — see the upstream
- // proposal in docs/third-party-htlc.md
if (type === 0x08) return {
- type: 'htlc', claimPubkey: hex.encode(r.bytes(33)), refundPubkey: hex.encode(r.bytes(33)),
+ type: 'serverHtlcRecv', userPubkey: hex.encode(r.bytes(33)),
+ paymentHash: hex.encode(r.bytes(32)), htlcExpiry: r.u32(), htlcExpiryDelta: r.u16(),
+ };
+ if (type === 0x09) return {
+ type: 'serverHtlcSend', userPubkey: hex.encode(r.bytes(33)),
paymentHash: hex.encode(r.bytes(32)), htlcExpiry: r.u32(),
};
throw new Error('unknown vtxo policy type ' + type);
diff --git a/src/ark/refresh.js b/src/ark/refresh.js
index adf4c85..40c6faa 100644
--- a/src/ark/refresh.js
+++ b/src/ark/refresh.js
@@ -47,9 +47,12 @@ const timelockSignScript = (height, xonlyKey) =>
concatBytes(pushInt(height), Uint8Array.of(0xb1, 0x75, 0x20), xonlyKey, Uint8Array.of(0xac));
const delayedSignScript = (delta, xonlyKey) =>
concatBytes(pushInt(delta), Uint8Array.of(0xb2, 0x75, 0x20), xonlyKey, Uint8Array.of(0xac));
-// OP_HASH160 <ripemd160(hash)> OP_EQUALVERIFY <xonly> OP_CHECKSIG
+// v0: OP_HASH160 <ripemd160(hash)> OP_EQUALVERIFY <xonly> OP_CHECKSIG
const hashSignScript = (unlockHash, xonlyKey) =>
concatBytes(Uint8Array.of(0xa9, 0x14), ripemd160(unlockHash), Uint8Array.of(0x88, 0x20), xonlyKey, Uint8Array.of(0xac));
+// v1 (pver 5) prefixes OP_SIZE <32> OP_EQUALVERIFY — the preimage must be 32 bytes
+const hashSignScriptV1 = (unlockHash, xonlyKey) =>
+ concatBytes(Uint8Array.of(0x82, 0x01, 0x20, 0x88), hashSignScript(unlockHash, xonlyKey));
export const tapLeafHash = (script) =>
taggedHash('TapLeaf', Uint8Array.of(0xc0), varint(script.length), script);
@@ -82,11 +85,13 @@ function cosignedInputTaproot(pubkeys, serverPub, expiryHeight) {
}
// HashLockedCosigned transition input: hArk leaf output —
-// internal = musig(user, server); leaves: expiry (server) + unlock (agg key)
-export function harkLeafTaproot(userPub, serverPub, expiryHeight, unlockHash) {
+// internal = musig(user, server); leaves: expiry (server) + unlock (agg key).
+// `gen` picks the unlock-script generation: 1 (0.6.0, preimage size check)
+// for new rounds, 0 for reconstructing vtxos minted before the upgrade.
+export function harkLeafTaproot(userPub, serverPub, expiryHeight, unlockHash, gen = 1) {
const { internalXOnly } = musigInternalKey([userPub, serverPub]);
const expiryLeaf = tapLeafHash(timelockSignScript(expiryHeight, serverPub.slice(1)));
- const unlockScript = hashSignScript(unlockHash, internalXOnly);
+ const unlockScript = (gen === 1 ? hashSignScriptV1 : hashSignScript)(unlockHash, internalXOnly);
const unlockLeaf = tapLeafHash(unlockScript);
// expiryLeaf doubles as the merkle path when spending through the unlock leaf
return { ...taprootFromMerkle(internalXOnly, tapBranch(expiryLeaf, unlockLeaf)), unlockScript, internalXOnly, expiryLeaf };
@@ -94,10 +99,11 @@ export function harkLeafTaproot(userPub, serverPub, expiryHeight, unlockHash) {
// Forfeit claim output: internal = musig(user, server);
// leaves: delayed exit (user) + unlock (server key)
+// Only built during live round participation, which is pver 5 — always v1.
function forfeitClaimTaproot(userPub, serverPub, exitDelta, unlockHash) {
const { internalXOnly } = musigInternalKey([userPub, serverPub]);
const exitLeaf = tapLeafHash(delayedSignScript(exitDelta, userPub.slice(1)));
- const unlockLeaf = tapLeafHash(hashSignScript(unlockHash, serverPub.slice(1)));
+ const unlockLeaf = tapLeafHash(hashSignScriptV1(unlockHash, serverPub.slice(1)));
return taprootFromMerkle(internalXOnly, tapBranch(exitLeaf, unlockLeaf));
}
@@ -113,9 +119,10 @@ export function transitionInputTxout(transition, amountSat, serverPub, expiryHei
const pubkeys = transition.pubkeys.map((p) => hex.decode(p));
return { valueSat: amountSat, scriptPubKey: cosignedInputTaproot(pubkeys, serverPub, expiryHeight).scriptPubKey };
}
- if (transition.type === 'hashLockedCosigned') {
+ if (transition.type === 'hashLockedCosigned' || transition.type === 'hashLockedCosigned_v0') {
const hash = hex.decode(transition.unlock.hash ?? sha256Hex(transition.unlock.preimage));
- const tp = harkLeafTaproot(hex.decode(transition.userPubkey), serverPub, expiryHeight, hash);
+ const gen = transition.type === 'hashLockedCosigned' ? 1 : 0;
+ const tp = harkLeafTaproot(hex.decode(transition.userPubkey), serverPub, expiryHeight, hash, gen);
return { valueSat: amountSat, scriptPubKey: tp.scriptPubKey };
}
if (transition.type === 'arkoor') {
@@ -242,10 +249,11 @@ const encodeTransition = (t) => {
return concatBytes(Uint8Array.of(0x02), varint(t.clientCosigners.length),
...t.clientCosigners.map((p) => hex.decode(p)), hex.decode(t.tapTweak), sig);
}
- if (t.type === 'hashLockedCosigned') {
+ if (t.type === 'hashLockedCosigned' || t.type === 'hashLockedCosigned_v0') {
const [tag, val] = t.unlock.preimage != null
? [0, hex.decode(t.unlock.preimage)] : [1, hex.decode(t.unlock.hash)];
- return concatBytes(Uint8Array.of(0x03), hex.decode(t.userPubkey), sig, Uint8Array.of(tag), val);
+ const typeByte = t.type === 'hashLockedCosigned' ? 0x04 : 0x03;
+ return concatBytes(Uint8Array.of(typeByte), hex.decode(t.userPubkey), sig, Uint8Array.of(tag), val);
}
throw new Error('unknown transition type ' + t.type);
};
@@ -320,7 +328,8 @@ export async function roundParticipationStatus(ark, unlockHash) {
// MuSig2 script-spend cosign of the hash-locked leaf (NO taptweak)
export async function cosignHarkLeaf(ark, vtxo, fundingTx, vtxoKeys, serverPub) {
const last = vtxo.genesis[vtxo.genesis.length - 1];
- if (last.transition.type !== 'hashLockedCosigned') throw new Error('not a hark leaf vtxo');
+ if (!['hashLockedCosigned', 'hashLockedCosigned_v0'].includes(last.transition.type)) throw new Error('not a hark leaf vtxo');
+ const leafGen = last.transition.type === 'hashLockedCosigned' ? 1 : 0;
const unlockHash = hex.decode(last.transition.unlock.hash);
// leaf tx + the output it spends
@@ -330,7 +339,7 @@ export async function cosignHarkLeaf(ark, vtxo, fundingTx, vtxoKeys, serverPub)
? chain[chain.length - 2].tx.outputs[chain[chain.length - 2].outputIdx]
: fundingTx.outputs[vtxo.anchorPoint.vout];
- const tp = harkLeafTaproot(vtxoKeys.pubkey, serverPub, vtxo.expiryHeight, unlockHash);
+ const tp = harkLeafTaproot(vtxoKeys.pubkey, serverPub, vtxo.expiryHeight, unlockHash, leafGen);
const sighash = taprootScriptSighash(leaf.tx, [preleafTxout], tapLeafHash(tp.unlockScript));
const nonces = musig2.nonceGen(vtxoKeys.pubkey, vtxoKeys.privkey, undefined, sighash);
diff --git a/src/ark/send.js b/src/ark/send.js
index afdb4b9..0e63c7c 100644
--- a/src/ark/send.js
+++ b/src/ark/send.js
@@ -15,10 +15,8 @@ import { concatBytes, reader, grpcCall, pbWriter, pbFields } from './proto.js';
const asBytes = (v) => (typeof v === 'string' ? hex.decode(v) : Uint8Array.from(v));
// The key that cosigns arkoor spends of a vtxo with this policy (bark's
-// VtxoPolicy::arkoor_pubkey). For a third-party HTLC that's the claimer, who
-// holds the taproot keyspend path together with the server.
-export const policyOwnerPubkey = (policy) =>
- policy.type === 'htlc' ? policy.claimPubkey : policy.userPubkey;
+// VtxoPolicy::arkoor_pubkey).
+export const policyOwnerPubkey = (policy) => policy.userPubkey;
const te = new TextEncoder();
@@ -57,7 +55,7 @@ function pushInt(n) {
return Uint8Array.of(bytes.length, ...bytes);
}
-const OP = { CSV: 0xb2, CLTV: 0xb1, DROP: 0x75, CHECKSIG: 0xac, HASH160: 0xa9, EQUALVERIFY: 0x88 };
+const OP = { CSV: 0xb2, CLTV: 0xb1, DROP: 0x75, CHECKSIG: 0xac, HASH160: 0xa9, EQUALVERIFY: 0x88, SIZE: 0x82 };
export const P2A_SCRIPT = hex.decode('51024e73');
// delayed_sign: <csv> OP_CSV OP_DROP <xonly> OP_CHECKSIG
@@ -68,12 +66,20 @@ const delayedSignScript = (delta, xonly) =>
const timelockSignScript = (height, xonly) =>
concatBytes(pushInt(height), Uint8Array.of(OP.CLTV, OP.DROP, 0x20), xonly, Uint8Array.of(OP.CHECKSIG));
-// hash_delay_sign: <csv> OP_CSV OP_DROP OP_HASH160 <ripemd160(hash)> OP_EQUALVERIFY <xonly> OP_CHECKSIG
+// hash_delay_sign_v0: <csv> OP_CSV OP_DROP OP_HASH160 <ripemd160(hash)> OP_EQUALVERIFY <xonly> OP_CHECKSIG
// (the witness reveals the 32B preimage; HASH160(preimage) = ripemd160(sha256(preimage)))
const hashDelaySignScript = (paymentHash, delta, xonly) => concatBytes(
pushInt(delta), Uint8Array.of(OP.CSV, OP.DROP, OP.HASH160, 0x14), ripemd160(paymentHash),
Uint8Array.of(OP.EQUALVERIFY, 0x20), xonly, Uint8Array.of(OP.CHECKSIG));
+// hash_delay_sign (v1, pver 5): same, but the preimage's size is checked to be
+// 32 bytes before hashing — OP_SIZE <32> OP_EQUALVERIFY inserted after the
+// timelock, mirroring Lightning's own on-chain check.
+const hashDelaySignScriptV1 = (paymentHash, delta, xonly) => concatBytes(
+ pushInt(delta), Uint8Array.of(OP.CSV, OP.DROP, OP.SIZE, 0x01, 0x20, OP.EQUALVERIFY, OP.HASH160, 0x14),
+ ripemd160(paymentHash),
+ Uint8Array.of(OP.EQUALVERIFY, 0x20), xonly, Uint8Array.of(OP.CHECKSIG));
+
// delay_timelock_sign: <height> OP_CLTV OP_DROP <csv> OP_CSV OP_DROP <xonly> OP_CHECKSIG
const delayTimelockSignScript = (height, delta, xonly) => concatBytes(
pushInt(height), Uint8Array.of(OP.CLTV, OP.DROP), pushInt(delta),
@@ -138,33 +144,27 @@ export const checkpointPolicyTaproot = (userPub, serverPub, expiryHeight) =>
// Taproot for any user-facing VTXO policy (mirror of bark's VtxoPolicy::taproot).
// policy fields may be hex strings (decoded form) or byte arrays.
export function policyTaproot(policy, serverPub, exitDelta) {
- if (policy.type === 'htlc') {
- // third-party HTLC: keyspend musig(claimer, server)
- // leaf 1: claimer spends with the preimage after exit_delta
- // leaf 2: refunder spends after htlc expiry + 2*exit_delta
- const claim = asBytes(policy.claimPubkey);
- const refund = asBytes(policy.refundPubkey);
- const hash = asBytes(policy.paymentHash);
- const claimLeaf = hashDelaySignScript(hash, exitDelta, xonly(claim));
- const refundLeaf = delayTimelockSignScript(policy.htlcExpiry, 2 * exitDelta, xonly(refund));
- return { ...taprootFromLeaves(claim, serverPub, [claimLeaf, refundLeaf]), claimLeaf, refundLeaf };
- }
const user = asBytes(policy.userPubkey);
if (policy.type === 'pubkey') return pubkeyPolicyTaproot(user, serverPub, exitDelta);
- if (policy.type === 'serverHtlcSend') {
+ // The bare types are the 0.6.0 (pver 5) policies whose hashlock leaves
+ // check the preimage size; _v0 keeps building the legacy scripts so vtxos
+ // minted before the upgrade still reconstruct, sign, and exit correctly.
+ if (policy.type === 'serverHtlcSend' || policy.type === 'serverHtlcSend_v0') {
+ const hashDelay = policy.type === 'serverHtlcSend' ? hashDelaySignScriptV1 : hashDelaySignScript;
const hash = asBytes(policy.paymentHash);
// leaf 1: server spends with preimage after exit_delta
// leaf 2: user refund after htlc expiry + 2*exit_delta delay
- const serverClaim = hashDelaySignScript(hash, exitDelta, xonly(serverPub));
+ const serverClaim = hashDelay(hash, exitDelta, xonly(serverPub));
const userRefund = delayTimelockSignScript(policy.htlcExpiry, 2 * exitDelta, xonly(user));
return { ...taprootFromLeaves(user, serverPub, [serverClaim, userRefund]), serverClaim, userRefund };
}
- if (policy.type === 'serverHtlcRecv') {
+ if (policy.type === 'serverHtlcRecv' || policy.type === 'serverHtlcRecv_v0') {
+ const hashDelay = policy.type === 'serverHtlcRecv' ? hashDelaySignScriptV1 : hashDelaySignScript;
const hash = asBytes(policy.paymentHash);
// leaf 1: server reclaim after htlc expiry + exit_delta delay
// leaf 2: user spends with preimage after htlc_expiry_delta + exit_delta
const serverRefund = delayTimelockSignScript(policy.htlcExpiry, exitDelta, xonly(serverPub));
- const userClaim = hashDelaySignScript(hash, policy.htlcExpiryDelta + exitDelta, xonly(user));
+ const userClaim = hashDelay(hash, policy.htlcExpiryDelta + exitDelta, xonly(user));
return { ...taprootFromLeaves(user, serverPub, [serverRefund, userClaim]), serverRefund, userClaim };
}
throw new Error('unsupported vtxo policy type ' + policy.type);
@@ -214,16 +214,19 @@ const encodePubkeyPolicy = (pub33) => concatBytes(Uint8Array.of(0x00), pub33);
// Serialize any user-facing VtxoPolicy (mirror of proto.js decodePolicy).
export function encodePolicy(p) {
- if (p.type === 'htlc') {
- return concatBytes(Uint8Array.of(0x08), asBytes(p.claimPubkey), asBytes(p.refundPubkey),
- asBytes(p.paymentHash), u32le(p.htlcExpiry));
- }
const user = asBytes(p.userPubkey);
if (p.type === 'pubkey') return encodePubkeyPolicy(user);
if (p.type === 'serverHtlcSend') {
- return concatBytes(Uint8Array.of(0x01), user, asBytes(p.paymentHash), u32le(p.htlcExpiry));
+ return concatBytes(Uint8Array.of(0x09), user, asBytes(p.paymentHash), u32le(p.htlcExpiry));
}
if (p.type === 'serverHtlcRecv') {
+ return concatBytes(Uint8Array.of(0x08), user, asBytes(p.paymentHash),
+ u32le(p.htlcExpiry), u16le(p.htlcExpiryDelta));
+ }
+ if (p.type === 'serverHtlcSend_v0') {
+ return concatBytes(Uint8Array.of(0x01), user, asBytes(p.paymentHash), u32le(p.htlcExpiry));
+ }
+ if (p.type === 'serverHtlcRecv_v0') {
return concatBytes(Uint8Array.of(0x02), user, asBytes(p.paymentHash),
u32le(p.htlcExpiry), u16le(p.htlcExpiryDelta));
}
diff --git a/src/ark/validate.js b/src/ark/validate.js
index d09f6ec..fae3465 100644
--- a/src/ark/validate.js
+++ b/src/ark/validate.js
@@ -77,10 +77,11 @@ export async function validateVtxo(vtxo, { serverPubkey, chain, expectPubkeys, a
const sig = hex.decode(t.signature);
let msg, key;
- if (t.type === 'hashLockedCosigned') {
+ if (t.type === 'hashLockedCosigned' || t.type === 'hashLockedCosigned_v0') {
// script-spend through the unlock clause; signed by the raw agg key
const hash = t.unlock.hash ? hex.decode(t.unlock.hash) : sha256(hex.decode(t.unlock.preimage));
- const tp = harkLeafTaproot(hex.decode(t.userPubkey), serverPubkey, vtxo.expiryHeight, hash);
+ const gen = t.type === 'hashLockedCosigned' ? 1 : 0;
+ const tp = harkLeafTaproot(hex.decode(t.userPubkey), serverPubkey, vtxo.expiryHeight, hash, gen);
msg = taprootScriptSighash(tx, [prevout], tapLeafHash(tp.unlockScript));
key = tp.internalXOnly;
if (!t.unlock.preimage) fail(`transition ${i} has no unlock preimage`);