// Native-JS delegated round refresh (hArk): submit participation, poll the // round result, MuSig2-cosign the hash-locked leaf, forfeit the old VTXO in // exchange for the unlock preimage, and finalize the refreshed VTXO. // // Mirrors bark's bark/src/round/mod.rs (join_next_round_delegated / // progress_delegated / hark_vtxo_swap) and lib/src/{forfeit.rs,tree/signed.rs}. import { hex } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; import { ripemd160 } from '@noble/hashes/ripemd160'; import { schnorr, secp256k1 } from '@noble/curves/secp256k1'; import * as musig2 from '@scure/btc-signer/musig2'; import { grpcCall, pbWriter, pbFields, concatBytes, reader } from './proto.js'; import { musigInternalKey, taprootSighash, txid, P2A_SCRIPT, pubkeyPolicyTaproot, policyTaproot } from './send.js'; const te = new TextEncoder(); 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; }; const u64be = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), false); return b; }; const u16le = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff); const varint = (n) => { if (n < 0xfd) return Uint8Array.of(n); if (n <= 0xffff) return concatBytes(Uint8Array.of(0xfd), u16le(n)); return concatBytes(Uint8Array.of(0xfe), u32le(n)); }; const taggedHash = (tag, ...data) => { const th = sha256(te.encode(tag)); return sha256(concatBytes(th, th, ...data)); }; // --------------------------------------------------------------------------- // scripts + taproots for round vtxo chain reconstruction // --------------------------------------------------------------------------- function pushInt(n) { if (n === 0) return Uint8Array.of(0x00); if (n >= 1 && n <= 16) return Uint8Array.of(0x50 + n); const bytes = []; let v = n; while (v > 0) { bytes.push(v & 0xff); v >>= 8; } if (bytes[bytes.length - 1] & 0x80) bytes.push(0x00); return Uint8Array.of(bytes.length, ...bytes); } 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)); // v0: OP_HASH160 OP_EQUALVERIFY 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); const byteCompare = (a, b) => { for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return a[i] - b[i]; } return 0; }; const tapBranch = (a, b) => { const [lo, hi] = byteCompare(a, b) <= 0 ? [a, b] : [b, a]; return taggedHash('TapBranch', lo, hi); }; function taprootFromMerkle(internalXOnly, merkleRoot) { const tapTweak = taggedHash('TapTweak', internalXOnly, ...(merkleRoot ? [merkleRoot] : [])); const P = secp256k1.ProjectivePoint; const internalPoint = P.fromHex(concatBytes(Uint8Array.of(0x02), internalXOnly)); const outputPoint = internalPoint.add(P.BASE.multiply(BigInt('0x' + hex.encode(tapTweak)))); const outputCompressed = outputPoint.toRawBytes(true); const outputXOnly = outputCompressed.slice(1); const outputParity = outputCompressed[0] === 0x03 ? 1 : 0; return { tapTweak, outputXOnly, outputParity, scriptPubKey: concatBytes(hex.decode('5120'), outputXOnly) }; } // Cosigned transition input: internal = musig(pubkeys), one timelock leaf function cosignedInputTaproot(pubkeys, serverPub, expiryHeight) { const { internalXOnly } = musigInternalKey(pubkeys); const leaf = tapLeafHash(timelockSignScript(expiryHeight, serverPub.slice(1))); return taprootFromMerkle(internalXOnly, leaf); } // HashLockedCosigned transition input: hArk leaf output — // 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 = (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 }; } // 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(hashSignScriptV1(unlockHash, serverPub.slice(1))); return taprootFromMerkle(internalXOnly, tapBranch(exitLeaf, unlockLeaf)); } // --------------------------------------------------------------------------- // vtxo tx-chain reconstruction (VtxoTxIter) // --------------------------------------------------------------------------- const otherSum = (item) => item.feeSat + item.otherOutputs.reduce((n, o) => n + o.valueSat, 0); // A taproot output script is 34 bytes whatever key sits inside it. Callers // that only need transaction SIZES (fee estimates) pass sizeOnly and skip // the musig key aggregation — several secp256k1 multiplies per hop, which // across a wallet's coins was seconds of main-thread work at boot. const DUMMY_P2TR = concatBytes(hex.decode('5120'), new Uint8Array(32)); // The hark unlock witness's script + control block, shaped for size only. export function harkUnlockWitnessShape(unlockHash, gen = 1) { const unlockScript = (gen === 1 ? hashSignScriptV1 : hashSignScript)(unlockHash, new Uint8Array(32)); return { unlockScript, controlBlock: new Uint8Array(65) }; } export function transitionInputTxout(transition, amountSat, serverPub, expiryHeight, sizeOnly = false) { if (sizeOnly) { const known = ['cosigned', 'hashLockedCosigned', 'hashLockedCosigned_v0', 'arkoor']; if (!known.includes(transition.type)) throw new Error('input_txout unsupported for transition ' + transition.type); return { valueSat: amountSat, scriptPubKey: DUMMY_P2TR }; } if (transition.type === 'cosigned') { const pubkeys = transition.pubkeys.map((p) => hex.decode(p)); return { valueSat: amountSat, scriptPubKey: cosignedInputTaproot(pubkeys, serverPub, expiryHeight).scriptPubKey }; } if (transition.type === 'hashLockedCosigned' || transition.type === 'hashLockedCosigned_v0') { const hash = hex.decode(transition.unlock.hash ?? sha256Hex(transition.unlock.preimage)); 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') { // output key = musig(cosigners + server) with the stored x-only taptweak const keys = musig2.sortKeys([...transition.clientCosigners.map((p) => hex.decode(p)), serverPub]); const ctx = musig2.keyAggregate(keys, [hex.decode(transition.tapTweak)], [true]); const outputXOnly = musig2.keyAggExport(ctx); return { valueSat: amountSat, scriptPubKey: concatBytes(hex.decode('5120'), outputXOnly) }; } throw new Error('input_txout unsupported for transition ' + transition.type); } const sha256Hex = (h) => hex.encode(sha256(hex.decode(h))); export const vtxoAnchorAmount = (vtxo) => vtxo.amountSat + vtxo.genesis.reduce((n, it) => n + otherSum(it), 0); // Walk the genesis chain, returning [{tx, outputIdx}] like Vtxo::transactions() export function vtxoTransactions(vtxo, serverPub, sizeOnly = false) { const anchorAmount = vtxoAnchorAmount(vtxo); let prev = vtxo.anchorPoint.raw; let currentAmount = anchorAmount; const items = []; for (let i = 0; i < vtxo.genesis.length; i++) { const item = vtxo.genesis[i]; const nextAmount = currentAmount - otherSum(item); const nextOutput = i + 1 < vtxo.genesis.length ? transitionInputTxout(vtxo.genesis[i + 1].transition, nextAmount, serverPub, vtxo.expiryHeight, sizeOnly) : { valueSat: vtxo.amountSat, scriptPubKey: sizeOnly ? DUMMY_P2TR : policyTaproot(vtxo.policy, serverPub, vtxo.exitDelta).scriptPubKey }; // decoded txouts carry hex-string scripts — normalize to bytes const others = item.otherOutputs.map((o) => ({ valueSat: o.valueSat, scriptPubKey: hex.decode(o.scriptPubKey) })); const outs = [ ...others.slice(0, item.outputIdx), nextOutput, ...others.slice(item.outputIdx), { valueSat: item.feeSat, scriptPubKey: P2A_SCRIPT }, ]; const tx = { version: 3, locktime: 0, inputs: [{ prevout: prev, sequence: 0 }], outputs: outs }; const id = txid(tx); prev = concatBytes(id, u32le(item.outputIdx)); currentAmount = nextAmount; items.push({ tx, outputIdx: item.outputIdx }); } return items; } // BIP-341 script-spend sighash (SIGHASH_DEFAULT, single input, no annex) export function taprootScriptSighash(tx, prevouts, leafHash) { const base = taprootSighashFields(tx, prevouts); return taggedHash('TapSighash', ...base, Uint8Array.of(0x02), // spend type: ext_flag=1 (script), no annex u32le(0), // input index leafHash, Uint8Array.of(0x00), u32le(0xffffffff), // ext: leaf, key ver, codesep ); } function taprootSighashFields(tx, prevouts) { const shaPrevouts = sha256(concatBytes(...tx.inputs.map((i) => i.prevout))); const shaAmounts = sha256(concatBytes(...prevouts.map((p) => u64le(p.valueSat)))); const shaScripts = sha256(concatBytes(...prevouts.map((p) => concatBytes(varint(p.scriptPubKey.length), p.scriptPubKey)))); const shaSequences = sha256(concatBytes(...tx.inputs.map((i) => u32le(i.sequence)))); const shaOutputs = sha256(concatBytes(...tx.outputs.map((o) => concatBytes(u64le(o.valueSat), varint(o.scriptPubKey.length), o.scriptPubKey)))); return [Uint8Array.of(0x00), Uint8Array.of(0x00), u32le(tx.version), u32le(tx.locktime), shaPrevouts, shaAmounts, shaScripts, shaSequences, shaOutputs]; } // --------------------------------------------------------------------------- // consensus tx parsing (for the round funding tx) // --------------------------------------------------------------------------- export function parseTx(bytes) { const r = reader(bytes); const version = r.u32(); let marker = false; let inCount = r.compactSize(); if (inCount === 0) { // segwit marker const flag = r.u8(); if (flag !== 1) throw
const { DataTypes } = require('sequelize');

const attributes = {
  id: {
    type: DataTypes.INTEGER(11),
    allowNull: false,
    defaultValue: null,
    primaryKey: true,
    autoIncrement: true,
    comment: null,
    field: "id"
  },
  admin: {
    type: DataTypes.BOOLEAN,
    allowNull: true,
    defaultValue: false,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "admin"
  },
  locked: {
    type: DataTypes.BOOLEAN,
    allowNull: true,
    defaultValue: false,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "locked"
  },
  verified: {
    type: DataTypes.STRING,
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "verified"
  },
  fiat: {
    type: DataTypes.BOOLEAN,
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "fiat"
  },
  index: {
    type: DataTypes.INTEGER,
    allowNull: true,
    defaultValue: 0,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "index"
  },
  ip: {
    type: DataTypes.INTEGER.UNSIGNED,
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "ip"
  },
  seed: {
    type: DataTypes.STRING(255),
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "seed"
  },
  username: {
    type: DataTypes.STRING(255),
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "username"
  },
  password: {
    type: DataTypes.STRING(255),
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "password"
  },
  unit: {
    type: DataTypes.STRING(255),
    allowNull: true,
    defaultValue: 'SAT',
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "unit"
  },
  account_id: {
    type: DataTypes.INTEGER(11),
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "account_id"
  },
  otpsecret: {
    type: DataTypes.STRING(255),
    allowNull: true,
    defaultValue: "CAD",
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "otpsecret"
  },
  currency: {
    type: DataTypes.STRING(255),
    allowNull: true,
    defaultValue: "CAD",
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "currency"
  },
  currencies: {
    type: DataTypes.JSON,
    allowNull: false,
    defaultValue: ["USD"],
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "currencies"
  },
  createdAt: {
    type: DataTypes.DATE,
    allowNull: false,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "createdAt"
  },
  updatedAt: {
    type: DataTypes.DATE,
    allowNull: false,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "updatedAt"
  },
  subscriptions: {
    type: DataTypes.TEXT,
    get: function() {
      return JSON.parse(this.getDataValue("subscriptions"));
    },
    set: function(value) {
      return this.setDataValue("subscriptions", JSON.stringify(value));
    },
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "subscriptions"
  },
  twofa: {
    type: DataTypes.INTEGER(1),
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "twofa"
  },
  pin: {
    type: DataTypes.STRING(255),
    allowNull: true,
    defaultValue: null,
    primaryKey: false,
    autoIncrement: false,
    comment: null,
    field: "pin"
  },
  email: {
    type: DataTypes.STRING(255)
  },
  phone: {
    type: DataTypes.STRING(255)
  }
};

const options = {
  tableName: "users",
  comment: "",
  indexes: []
};

db["User"] = db.define("users_model", attributes, options);