WarpCore / Documentation

Atomic PvP

SOURCE · docs/specs/atomic-pvp-v1.md · synced from the engineering repo

Atomic Payment-versus-Payment — Specification v1

Status: Draft for FX desk review, tn10 implementation Crate: warpcore-pvp Document version: 2026-04-11 Target platform: Kaspa testnet-10 today, mainnet (Toccata is live there since ~30 June 2026; WarpCore deployment gated on audit)

1. Abstract

Payment-versus-payment (PvP) is the settlement pattern FX desks need most and get least: the promise that both legs of a cross-currency trade commit atomically, so neither counterparty is ever exposed. CLS Bank settles ~$6T/day this way, but only for 18 currencies, only for member banks, and only in batched T+0 windows.

This specification defines an atomic PvP settlement primitive on Kaspa. Given two matched legs — “Bank A sends X AED to Bank B” and “Bank B sends Y USD to Bank A” at an agreed rate — the protocol commits both legs atomically in a single on-chain event or not at all.

2. Why this is possible on Kaspa today

Kaspa’s current tn10 scripting supports enough opcodes to build a hash-time-locked contract (HTLC): OP_SHA256, OP_EQUALVERIFY, OP_CHECKSIG, OP_CHECKLOCKTIMEVERIFY. The SettlementVault covenant in warpcore-kaspa::covenant_builder compiles these into a script that enforces:

IF <secret> OP_SHA256 <known_hash> OP_EQUALVERIFY <counterparty_key> OP_CHECKSIG
ELSE <timeout_blocks> OP_CHECKLOCKTIMEVERIFY OP_DROP <originator_key> OP_CHECKSIG
ENDIF

The IF branch: anyone who knows the secret preimage AND holds the counterparty’s key can spend the UTXO. The ELSE branch: after timeout_blocks, the original funder can reclaim with their own signature.

Atomic PvP uses TWO settlement-vault UTXOs, one per leg, both bound to the same secret hash. Revealing the secret unlocks both legs simultaneously.

3. Atomicity semantics

The critical question: what happens if one leg clears and the other doesn’t? This specification is explicit about what atomicity means on each platform version.

3.1 tn10 (pre-hardfork) — batch-inclusion atomicity

On tn10, atomicity is enforced at the Kaspa transaction batch inclusion layer. The settlement worker constructs a single Kaspa transaction that spends both covenant UTXOs in the same tx body. Kaspa either includes the whole transaction or rejects it. Partial inclusion is impossible.

Failure modes on tn10:

  • Both legs commit. Happy path. Both parties receive their counterparty’s currency.
  • Neither leg commits. Transaction rejected (insufficient fee, malformed, pubkey mismatch). No state change.
  • Timeout reached before reveal. Either party can publish a refund transaction that unwinds their own leg via the ELSE branch. Both parties MUST unwind symmetrically — otherwise the one who unwinds first would be exposed. This is handled by the orchestrator’s reveal coordinator.

What tn10 does not protect against: an attacker who bribes a miner to include one leg’s tx but not the other. This is theoretically possible on any UTXO chain without script introspection. In practice, Kaspa’s GHOSTDAG and high block rate make this attack economically infeasible for any realistic settlement amount — the attacker would need to outbid the honest fee and hope the DAG orders the partial inclusion their way.

For amounts above $10M USD equivalent, the recommended mitigation is to wait for Toccata covenants (next section).

3.2 Post-hardfork — script-enforced atomicity

Since the Kaspa Toccata hardfork (activated on mainnet ~30 June 2026), the covenant can introspect the spending transaction and REQUIRE that both legs are present in the same transaction. The script gains access to operations like OP_SIG_CHECKTX_INPUT that let one UTXO’s spending rules reference another input in the same tx.

The updated script:

IF <secret> OP_SHA256 <known_hash> OP_EQUALVERIFY
   <peer_leg_utxo> OP_INPUT_EXISTS  // NEW: require the peer leg
   <counterparty_key> OP_CHECKSIG
ELSE <timeout_blocks> OP_CHECKLOCKTIMEVERIFY OP_DROP <originator_key> OP_CHECKSIG
ENDIF

Now a miner cannot include just one leg even if bribed: the script itself would reject the spend because the peer UTXO is not present in the same transaction. Atomicity becomes script-enforced, not inclusion-enforced.

The orchestrator API does not change between tn10 and post-hardfork. The caller submits the same two legs, the same secret, the same timeout. Only the internals of compile_real_covenant change. Clients get the upgrade for free.

4. Scope

The warpcore-pvp crate handles:

  • Validation. A PvpMatch must cross (leg_a’s debtor is leg_b’s creditor and vice versa) and use distinct currencies.
  • Secret generation. The PvpSecret type holds the 32-byte preimage and its SHA-256 hash.
  • Covenant compilation. Both legs’ settlement-vault scripts are compiled via the existing warpcore-kaspa::compile_real_covenant entry point, producing real tn10 opcode bytecode.
  • Plan assembly. A PvpPlan bundles the two compiled scripts, the shared secret hash, and metadata.

It does NOT handle:

  • FX rate quoting or matching (the legs arrive already matched)
  • Liquidity provisioning (the caller funds the UTXOs)
  • The actual Kaspa RPC submission (that’s the settlement worker)
  • 3+ party netting (deferred to v2)

5. Data model

5.1 PvpLeg

One side of a matched FX trade.

pub struct PvpLeg {
    pub leg_id: Uuid,
    pub debtor_bic: String,
    pub creditor_bic: String,
    pub currency: String,
    pub amount: Decimal,
    pub debtor_pubkey_hex: String,   // 32 bytes, 64 hex chars
    pub creditor_pubkey_hex: String, // 32 bytes, 64 hex chars
}

5.2 PvpMatch

Two legs whose directions and currencies cross.

pub struct PvpMatch {
    pub leg_a: PvpLeg,
    pub leg_b: PvpLeg,
}

Validation invariants:

  • leg_a.debtor_bic == leg_b.creditor_bic
  • leg_a.creditor_bic == leg_b.debtor_bic
  • leg_a.currency != leg_b.currency
  • Both legs have positive amounts
  • Both legs have valid 64-hex-char pubkeys

5.3 PvpSecret

32-byte preimage plus SHA-256 hash.

pub struct PvpSecret {
    pub preimage: [u8; 32],
    pub hash: [u8; 32],
}

The preimage is private until the reveal moment. The hash is public and embedded in both legs’ covenants.

5.4 PvpPlan

The orchestrator’s output.

pub struct PvpPlan {
    pub plan_id: Uuid,
    pub match_: PvpMatch,
    pub leg_a_script: Vec<u8>,   // Real tn10 opcode bytecode
    pub leg_b_script: Vec<u8>,   // Real tn10 opcode bytecode
    pub secret_hash: [u8; 32],
    pub timeout_blocks: u64,
    pub built_at: DateTime<Utc>,
}

6. Orchestrator flow

          ┌──────────────────┐
          │  PvpMatch input  │
          └────────┬─────────┘
                   │  validate()

          ┌──────────────────┐
          │   PvpSecret      │  (generated by caller)
          │   preimage+hash  │
          └────────┬─────────┘


       ┌──────────────────────────┐
       │  compile_real_covenant   │
       │  for leg_a (party A/B)   │───▶ leg_a_script
       └──────────────────────────┘


       ┌──────────────────────────┐
       │  compile_real_covenant   │
       │  for leg_b (party B/A)   │───▶ leg_b_script
       └──────────────────────────┘


          ┌──────────────────┐
          │     PvpPlan      │
          └──────────────────┘


          ┌──────────────────┐
          │ Settlement worker│  (not in this crate)
          │  — fund leg_a    │
          │  — fund leg_b    │
          │  — reveal (batch)│
          └──────────────────┘

7. Failure and recovery

See docs/specs/failure-modes-v1.md §7.2 for the canonical failure mode. Summary:

TriggerBehaviour
Validation failsReturn PvpError::InvalidLeg or Unmatched; no state change
Covenant compilation failsReturn PvpError::Compile; no state change
One leg funded, other not, timeout reachedBoth parties’ refund branches fire independently
Tx submission failsSettlement worker retries with same scripts (plans are deterministic)
Miner partial-inclusion attack (tn10 only)Audit event flagged; amount > $10M should use post-hardfork path

8. Determinism and audit replay

The compiled script bytes for a given (PvpMatch, secret, timeout) triple are deterministic. This is critical for audit replay: a regulator can reconstruct the expected scripts from the archival audit bundle and verify them against the on-chain UTXO scripts.

Tested in tests/orchestrator.rs::scripts_deterministic_for_identical_inputs.

9. HTTP API (future — not in this crate)

The orchestrator is a library. An HTTP endpoint wrapping it would look like:

POST /v1/settlement/pvp/build-plan
Authorization: Bearer <JWT, Operator or Admin>
Content-Type: application/json

{
  "leg_a": { ... },
  "leg_b": { ... },
  "timeout_blocks": 1000
}

Response:

{
  "plan_id": "<UUID>",
  "secret_hash_hex": "abcd...",
  "leg_a_script_hex": "63206c...",
  "leg_b_script_hex": "63206d...",
  "built_at": "2026-04-11T..."
}

The preimage is NOT returned — the caller supplies it at execution time and it never leaves the orchestrator’s memory. See v2 for the full reveal-side API (POST /v1/settlement/pvp/reveal).

10. Test vector coverage

TestAsserts
secret_hash_is_sha256_of_preimageSHA-256 derivation
build_plan_for_valid_aed_usd_matchHappy path, non-empty distinct scripts
both_scripts_embed_same_secret_hashBoth legs bind to same hash
mismatched_legs_rejected_directionDirection validation
same_currency_legs_rejectedPvP requires distinct currencies
invalid_pubkey_rejectedPubkey format validation
negative_amount_rejectedAmount > 0 enforced
two_plans_have_distinct_idsPlan IDs are unique
scripts_deterministic_for_identical_inputsCompiled bytes are deterministic
self_transfer_rejectedSelf-transfer legs rejected

10 tests, all passing.

11. Reference implementation

  • Crate: warpcore-pvp
  • Key files:
    • src/lib.rsPvpLeg, PvpMatch, PvpSecret, PvpPlan, PvpOrchestrator, errors
    • tests/orchestrator.rs — 10 passing integration tests
  • Dependencies:
    • warpcore-kaspa for compile_real_covenant and SettlementVaultParams
    • warpcore-hsm (used indirectly via the broader settlement path)

12. Roadmap

VersionPlanned
v1 (this doc)Two-party PvP on tn10 with HTLC semantics
v1.1HTTP endpoint POST /v1/settlement/pvp/build-plan + reveal endpoint
v1.2Integration with warpcore-uca so every PvP carries a UCA
v23+ party netting via Toccata covenant introspection (post-hardfork only)
v2.1Multi-leg CLS-style daily netting window
v3Streaming RFQ layer — quote and match integrated with build-plan

13. Benchmarks (to be added post-integration)

Target metrics for v1.1 (once wired to the settlement worker):

  • Plan build latency: < 5 ms p99
  • End-to-end commit latency (tn10): < 2 seconds p99
  • Timeout safety margin: 10× expected commit latency (default 1000 blocks ≈ 17 minutes)

Review: FX desk heads at sponsor banks