aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/Code.svelte
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-01-19 15:46:43 -0800
committerAdam Soltys <[email protected]>2024-01-19 15:46:43 -0800
commitce915dc0a6db8890f2393ca03c94caf16b4a54ed (patch)
treef1a626a35a21f764bb24b9c63ad5923a9c5245e6 /src/components/Code.svelte
parent3dbeeb10d6d94d16b506af3fcc43a23614fae84b (diff)
downloadcoinos-ui-ce915dc0a6db8890f2393ca03c94caf16b4a54ed.tar.gz
coinos-ui-ce915dc0a6db8890f2393ca03c94caf16b4a54ed.zip
hide seed phrase
Diffstat (limited to 'src/components/Code.svelte')
0 files changed, 0 insertions, 0 deletions
*/ .highlight .nf { color: #06B; font-weight: bold } /* Name.Function */ .highlight .nl { color: #369; font-style: italic } /* Name.Label */ .highlight .nn { color: #B06; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #369; font-weight: bold } /* Name.Property */ .highlight .nt { color: #B06; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #369 } /* Name.Variable */ .highlight .ow { color: #080 } /* Operator.Word */ .highlight .w { color: #BBB } /* Text.Whitespace */ .highlight .mb { color: #00D; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #00D; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #00D; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #00D; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #00D; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #D20; background-color: #FFF0F0 } /* Literal.String.Affix */ .highlight .sb { color: #D20; background-color: #FFF0F0 } /* Literal.String.Backtick */ .highlight .sc { color: #D20; background-color: #FFF0F0 } /* Literal.String.Char */ .highlight .dl { color: #D20; background-color: #FFF0F0 } /* Literal.String.Delimiter */ .highlight .sd { color: #D20; background-color: #FFF0F0 } /* Literal.String.Doc */ .highlight .s2 { color: #D20; background-color: #FFF0F0 } /* Literal.String.Double */ .highlight .se { color: #04D; background-color: #FFF0F0 } /* Literal.String.Escape */ .highlight .sh { color: #D20; background-color: #FFF0F0 } /* Literal.String.Heredoc */ .highlight .si { color: #33B; background-color: #FFF0F0 } /* Literal.String.Interpol */ .highlight .sx { color: #2B2; background-color: #F0FFF0 } /* Literal.String.Other */ .highlight .sr { color: #080; background-color: #FFF0FF } /* Literal.String.Regex */ .highlight .s1 { color: #D20; background-color: #FFF0F0 } /* Literal.String.Single */ .highlight .ss { color: #A60; background-color: #FFF0F0 } /* Literal.String.Symbol */ .highlight .bp { color: #038 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #06B; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #369 } /* Name.Variable.Class */ .highlight .vg { color: #D70 } /* Name.Variable.Global */ .highlight .vi { color: #33B } /* Name.Variable.Instance */ .highlight .vm { color: #369 } /* Name.Variable.Magic */ .highlight .il { color: #00D; font-weight: bold } /* Literal.Number.Integer.Long */
// Full receive-side VTXO validation: reconstruct the entire off-chain
// transaction chain from genesis data, pin it to a confirmed onchain anchor,
// and verify every transition signature. Mirrors bark's
// lib/src/vtxo/validation.rs.
//
// A VTXO that passes here is cryptographically spendable by us alone (given
// the exit-path delays), assuming the anchor doesn't get reorged and no
// prior arkoor double-spend was cosigned by the server before ours.

import { hex } from '@scure/base';
import { sha256 } from '@noble/hashes/sha256';
import { schnorr } from '@noble/curves/secp256k1';

import { taprootSighash, txid } from './send.js';
import {
  vtxoTransactions, vtxoAnchorAmount, transitionInputTxout,
  harkLeafTaproot, taprootScriptSighash, tapLeafHash, parseTx,
} from './refresh.js';

export class VtxoValidationError extends Error {}
const fail = (msg) => { throw new VtxoValidationError(msg); };

// chain: an object with getTxHex(txid) -> hex|null and tipHeight() -> number,
// plus getTxStatus(txid) -> {confirmed, block_height} (esplora semantics)
// allowPolicies: acceptable policy types (default plain pubkey only; lightning
// flows pass ['serverHtlcSend'] / ['serverHtlcRecv'] for their HTLC vtxos)
// acceptExpired: the server keeps honoring coins past their timelock (coinos:
// captaind allow_expired_arkoor) — the coin is then an IOU the server has
// already swept the tree of, which is exactly what the holder chose when they
// left custody to coinos; refuse only on servers that would refuse to spend it
export async function validateVtxo(vtxo, { serverPubkey, chain, expectPubkeys, allowPolicies = ['pubkey'], acceptExpired = false }) {
  const serverHex = hex.encode(serverPubkey);

  // -- structural --
  if (!allowPolicies.includes(vtxo.policy.type)) fail(`unsupported policy: ${vtxo.policy.type}`);
  if (vtxo.serverPubkey !== serverHex) fail('vtxo server pubkey does not match ours');
  if (expectPubkeys && !expectPubkeys.includes(vtxo.policy.userPubkey)) {
    fail('vtxo not paid to one of our keys');
  }
  if (!vtxo.genesis.length) fail('vtxo has no genesis transitions');

  // -- expiry --
  // Sub-dust vtxos outlive their expiryHeight: they have no unilateral exit
  // to protect (below dust there is no on-chain output they could become),
  // and the coinos ASP keeps honoring them off-chain after sweeping their
  // round — an explicit IOU where the trustless path never existed anyway.
  const tip = await chain.tipHeight();
  if (!acceptExpired && vtxo.expiryHeight <= tip && vtxo.amountSat >= 330) {
    fail(`vtxo expired at ${vtxo.expiryHeight}, tip ${tip}`);
  }

  // -- anchor: onchain, confirmed, and its output matches what the first
  //    transition claims to spend --
  const status = await chain.getTxStatus(vtxo.anchorPoint.txid);
  if (!status?.confirmed) fail(`anchor tx ${vtxo.anchorPoint.txid} not confirmed`);
  const anchorTx = parseTx(hex.decode(await chain.getTxHex(vtxo.anchorPoint.txid)));
  const anchorOut = anchorTx.outputs[vtxo.anchorPoint.vout];
  if (!anchorOut) fail('anchor vout does not exist');
  const expected = transitionInputTxout(
    vtxo.genesis[0].transition, vtxoAnchorAmount(vtxo), serverPubkey, vtxo.expiryHeight,
  );
  if (anchorOut.valueSat !== expected.valueSat
      || hex.encode(anchorOut.scriptPubKey) !== hex.encode(expected.scriptPubKey)) {
    fail('anchor output does not match the genesis chain');
  }

  // -- chain reconstruction must land exactly on the vtxo's outpoint --
  const items = vtxoTransactions(vtxo, serverPubkey);
  const last = items[items.length - 1];
  const lastTxid = hex.encode(txid(last.tx).slice().reverse());
  if (lastTxid !== vtxo.point.txid || last.outputIdx !== vtxo.point.vout) {
    fail(`reconstructed chain tip ${lastTxid}:${last.outputIdx} != vtxo point ${vtxo.id}`);
  }

  // -- every transition signature --
  let prevout = anchorOut;
  for (let i = 0; i < items.length; i++) {
    const t = vtxo.genesis[i].transition;
    const tx = items[i].tx;
    if (!t.signature) fail(`transition ${i} (${t.type}) is unsigned`);
    const sig = hex.decode(t.signature);

    let msg, key;
    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 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`);
      if (hex.encode(sha256(hex.decode(t.unlock.preimage))) !== hex.encode(hash)) {
        fail(`transition ${i} preimage does not match its hash`);
      }
    } else {
      // keyspend of the previous output — the prevout script itself carries
      // the taproot output key the signature must verify against
      msg = taprootSighash(tx, [prevout]);
      key = prevout.scriptPubKey.slice(2); // 5120 || xonly
    }
    if (!schnorr.verify(sig, msg, key)) fail(`transition ${i} (${t.type}) signature invalid`);
    prevout = tx.outputs[items[i].outputIdx];
  }

  return true;
}