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
PvpMatchmust cross (leg_a’s debtor is leg_b’s creditor and vice versa) and use distinct currencies. - Secret generation. The
PvpSecrettype 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_covenantentry point, producing real tn10 opcode bytecode. - Plan assembly. A
PvpPlanbundles 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_bicleg_a.creditor_bic == leg_b.debtor_bicleg_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:
| Trigger | Behaviour |
|---|---|
| Validation fails | Return PvpError::InvalidLeg or Unmatched; no state change |
| Covenant compilation fails | Return PvpError::Compile; no state change |
| One leg funded, other not, timeout reached | Both parties’ refund branches fire independently |
| Tx submission fails | Settlement 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
| Test | Asserts |
|---|---|
secret_hash_is_sha256_of_preimage | SHA-256 derivation |
build_plan_for_valid_aed_usd_match | Happy path, non-empty distinct scripts |
both_scripts_embed_same_secret_hash | Both legs bind to same hash |
mismatched_legs_rejected_direction | Direction validation |
same_currency_legs_rejected | PvP requires distinct currencies |
invalid_pubkey_rejected | Pubkey format validation |
negative_amount_rejected | Amount > 0 enforced |
two_plans_have_distinct_ids | Plan IDs are unique |
scripts_deterministic_for_identical_inputs | Compiled bytes are deterministic |
self_transfer_rejected | Self-transfer legs rejected |
10 tests, all passing.
11. Reference implementation
- Crate:
warpcore-pvp - Key files:
src/lib.rs—PvpLeg,PvpMatch,PvpSecret,PvpPlan,PvpOrchestrator, errorstests/orchestrator.rs— 10 passing integration tests
- Dependencies:
warpcore-kaspaforcompile_real_covenantandSettlementVaultParamswarpcore-hsm(used indirectly via the broader settlement path)
12. Roadmap
| Version | Planned |
|---|---|
| v1 (this doc) | Two-party PvP on tn10 with HTLC semantics |
| v1.1 | HTTP endpoint POST /v1/settlement/pvp/build-plan + reveal endpoint |
| v1.2 | Integration with warpcore-uca so every PvP carries a UCA |
| v2 | 3+ party netting via Toccata covenant introspection (post-hardfork only) |
| v2.1 | Multi-leg CLS-style daily netting window |
| v3 | Streaming 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