summaryrefslogtreecommitdiffstats
path: root/src/ark/board.js
blob: 4192716df85a6c7b7356d22ec94ad8d6fef290fa (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Native-JS board: move onchain funds into the Ark.
//
// Mirrors bark's lib/src/board.rs BoardBuilder: the funding output pays a
// taproot of musig(user, server) with a server-after-expiry sweep leaf (the
// same shape as the checkpoint policy), the exit tx is cosigned via MuSig2
// through RequestBoardCosign, and the resulting Vtxo<Full> has a single
// Cosigned genesis transition anchored at the funding outpoint.

import { hex, bech32m } from '@scure/base';
import { schnorr } from '@noble/curves/secp256k1';
import * as musig2 from '@scure/btc-signer/musig2';

import { grpcCall, pbWriter, pbFields, concatBytes } from './proto.js';
import { checkpointPolicyTaproot, pubkeyPolicyTaproot, taprootSighash, P2A_SCRIPT } from './send.js';

const u16le = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff);
const u32le = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff);
const u64le = (n) => {
  const b = new Uint8Array(8);
  new DataView(b.buffer).setBigUint64(0, BigInt(n), true);
  return b;
};

// 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.ceil((amountSat * (fees.ppm || 0)) / 1_000_000));

export const p2trAddress = (programXOnly, hrp = 'bcrt') =>
  bech32m.encode(hrp, [1, ...bech32m.toWords(programXOnly)], 1023);

// Build the board: funding output spec + exit tx + sighash.
// `fundingOutpointRaw` (36B) is set after the funding tx is known.
export function buildBoard({ userPub, serverPub, amountSat, feeSat, expiryHeight, exitDelta, fundingOutpointRaw }) {
  const fundingTaproot = checkpointPolicyTaproot(userPub, serverPub, expiryHeight);
  const exitTaproot = pubkeyPolicyTaproot(userPub, serverPub, exitDelta);

  const exitTx = {
    version: 3, locktime: 0,
    inputs: [{ prevout: fundingOutpointRaw, sequence: 0 }],
    outputs: [
      { valueSat: amountSat - feeSat, scriptPubKey: exitTaproot.scriptPubKey },
      { valueSat: feeSat, scriptPubKey: P2A_SCRIPT }, // board fee rides the anchor
    ],
  };
  const fundingPrevout = { valueSat: amountSat, scriptPubKey: fundingTaproot.scriptPubKey };
  const sighash = taprootSighash(exitTx, [fundingPrevout]);
  return { fundingTaproot, exitTaproot, exitTx, sighash };
}

export async function requestBoardCosign(ark, { amountSat, fundingOutpointRaw, expiryHeight, userPub, pubNonce, fundingTxBytes }) {
  const w = pbWriter();
  w.varintField(1, amountSat);
  w.bytesField(2, fundingOutpointRaw);
  w.varintField(3, expiryHeight);
  w.bytesField(4, userPub);
  w.bytesField(5, pubNonce);
  // Mandatory on mainnet since 0.6.0 (require_board_funding_tx): the server
  // validates the board outpoint against the funding tx and refuses inputs
  // that are VTXOs. The txid check ignores witnesses, so the broadcast
  // (signed) serialization is fine here.
  if (fundingTxBytes) w.bytesField(6, fundingTxBytes);
  const resp = await grpcCall(ark, 'bark_server.ArkService/RequestBoardCosign', w.finish());
  const out = {};
  for (const { field, value } of pbFields(resp)) {
    if (field === 1) out.pubNonce = value;
    if (field === 2) out.partialSig = value;
  }
  return out;
}

// MuSig2: combine our partial with the server's over the exit tx sighash.
export function combineBoardSignature({ board, serverCosign, userNonces, vtxoKeys, serverPub }) {
  const aggNonce = musig2.nonceAggregate([userNonces.public, serverCosign.pubNonce]);
  const session = new musig2.Session(
    aggNonce, board.fundingTaproot.sortedKeys, board.sighash, [board.fundingTaproot.tapTweak], [true],
  );
  const serverIdx = board.fundingTaproot.sortedKeys.findIndex(
    (k) => hex.encode(k) === hex.encode(serverPub));
  const noncesInKeyOrder = serverIdx === 0
    ? [serverCosign.pubNonce, userNonces.public] : [userNonces.public, serverCosign.pubNonce];
  if (!session.partialSigVerify(serverCosign.partialSig, noncesInKeyOrder, serverIdx)) {
    throw new Error('server board partial signature is invalid');
  }
  const userPartial = session.sign(userNonces.secret, vtxoKeys.privkey);
  const finalSig = session.partialSigAgg([userPartial, serverCosign.partialSig]);
  if (!schnorr.verify(finalSig, board.sighash, board.fundingTaproot.outputXOnly)) {
    throw new Error('combined board signature does not verify');
  }
  return finalSig;
}

// Vtxo<Full> bytes for the board: one Cosigned genesis transition.
export function encodeBoardVtxo({ userPub, serverPub, amountSat, feeSat, expiryHeight, exitDelta,
                                  fundingOutpointRaw, exitTxidInternal, finalSig }) {
  const cosignedTransition = concatBytes(
    Uint8Array.of(0x01),          // GenesisTransition::Cosigned
    Uint8Array.of(0x02), userPub, serverPub, // LengthPrefixedVector [user, server]
    finalSig,
  );
  return concatBytes(
    u16le(2),                     // encoding version
    u64le(amountSat - feeSat),
    u32le(expiryHeight),
    serverPub,
    u16le(exitDelta),
    fundingOutpointRaw,           // anchor point = funding utxo
    Uint8Array.of(0x01),          // one genesis item
    cosignedTransition,
    Uint8Array.of(0x01, 0x00),    // nb_outputs=1, output_idx=0
    u64le(feeSat),                // fee_amount
    Uint8Array.of(0x00), userPub, // policy: Pubkey(user)
    exitTxidInternal, u32le(0),   // point = exit_txid:0
  );
}

export async function registerBoardVtxo(ark, vtxoBytes) {
  const w = pbWriter();
  w.bytesField(1, vtxoBytes);
  await grpcCall(ark, 'bark_server.ArkService/RegisterBoardVtxo', w.finish());
}