asgayapedia

Covenant Version History: Phase 1 → v2.6

Purpose: Track covenant evolution from initial Phase 1 implementation through production v2.6.

Key insight: Evolution from complex (enforce everything on-chain) to simple (covenant enables, client enforces).


Quick Reference

Version Date Status Key Feature
Phase 1 2026-07-23 ✅ Tested MTP-only refund (baseline)
v2.0 2026-07-24 ✅ Tested Oracle-based refund (fast path)
v2.1 2026-07-24 ✅ Tested Price drop protection (7% threshold)
v2.2 2026-07-24 ✅ Tested Simplified refund (sender anytime)
v2.3 2026-07-26 ✅ Tested Seller buffer recovery
v2.4 2026-07-27 ✅ Tested Merchant cashout (killer feature)
v2.5 2026-07-27 ✅ PRODUCTION Refund anytime + all 4 paths tested
v2.6 2026-08-15 ✅ PRODUCTION Emergency abort + overlap zone (5 paths)

Phase 1: The Baseline (MTP-Only Refund)

Date: 2026-07-23
Status: ✅ Tested on chipnet
Archive: ARCHIVE_price-oracle_20260723.cash

What It Did

Basic price oracle covenant with MTP (Median Time Past) fallback for timeout:

function claim(sig recipientSig, datasig oracleSig, bytes oracleMessage) {
    // Verify oracle signature + timestamp
    // Calculate BCH amount from EUR price
    // Pay recipient + seller buffer
}

function refund(sig senderSig) {
    require(tx.time >= expiryMTP);  // MTP fallback
    // Refund to sender
}

What We Learned

✅ What worked:

❌ What didn’t work:

Critical bug discovered: Little-endian vs big-endian byte order! CashScript reads bytes as little-endian, but we were creating oracle messages as big-endian. 3 hours of debugging. 🐛

Fix: Changed writeBigInt64BE() → writeBigInt64LE() in oracle signature creation.

Chipnet Testing Results

Phase 1 complete! 🎉 First successful price oracle covenant on chipnet.


v2.0: Oracle-Based Refund (Fast Path)

Date: 2026-07-24
Status: ✅ Tested on chipnet
Motivation: Solve MTP slowness on chipnet

What Changed

Added oracle fast path for refund:

function refund(sig senderSig, datasig oracleSig, bytes oracleMessage) {
    require(checkSig(senderSig, sender));
    
    // Parse oracle timestamp
    int oracleTimestamp = int(oracleMessage.split(8)[0]);
    
    // Allow refund if EITHER condition met:
    bool oracleExpired = oracleTimestamp >= expiryOracleTime;
    bool mtpExpired = tx.time >= expiryMTP;
    
    require(oracleExpired || mtpExpired);
    
    // Refund to sender
}

Why This Was Better

✅ Improvements:

⚠️ Trade-offs:

Chipnet Testing Results

v2.0 validated: Oracle-based refund works! Fast testing possible.


v2.1: Price Drop Protection (7% Threshold)

Date: 2026-07-24
Status: ✅ Tested on chipnet
Motivation: Requirement #4 - Automatic abort on price drops >7%

What Changed

Added price floor enforcement in claim:

contract PriceOracle(
    // ... existing params ...
    int initialBchPriceInCents,  // NEW: Price when covenant funded
    int minPricePercent          // NEW: 93 = allow 7% drop max
)

function claim(...) {
    // Calculate current price from oracle
    int currentPrice = int(oracleMessage.split(8)[1]);
    
    // Calculate floor (93% of initial price)
    int priceFloor = (initialBchPriceInCents * minPricePercent) / 100;
    
    // Reject claim if price dropped too much
    require(currentPrice >= priceFloor);
    
    // ... rest of claim logic ...
}

Why This Was Needed

Problem: If BCH price drops 10% during payment window:

Solution: Covenant rejects claim if price < floor. Forces automatic refund + H€ minting.

Chipnet Testing Results

Test scenario: Initial price €1000/BCH, price drops to €920/BCH (-8%)

v2.1 validated: Price drop protection works! Covenant enforces 7% threshold.

⚠️ Limitation discovered: If price drops at t=5, sender must wait until t=60 (expiry) to refund. Can’t immediately recover BCH even though claim is impossible.


v2.2: Simplified Refund (Covenant Simplicity Principle)

Date: 2026-07-24
Status: ⏳ Deployed, testing in progress
Motivation: User sovereignty over safety theater

The Philosophical Shift

Realization: We kept adding complexity to handle edge cases. Moving logic to the client solved all issues at once.

Core principle:

Covenant = technical capability (“CAN refund anytime”)
Client = business logic (“SHOULD refund when appropriate”)

Tagline: The app enforces fairness. The covenant enforces ownership.

What Changed

Removed all conditions from refund:

function refund(sig senderSig) {
    require(checkSig(senderSig, sender));  // Just verify ownership
    
    // Output 0: Payment → sender
    // Output 1: Buffer → seller
}

Client enforces fairness:

// Client decides when auto-refund is appropriate
async function shouldAutoRefund() {
    const timeExpired = currentTime >= expiryTime;
    const priceDropped = currentPrice < priceFloor;
    return timeExpired || priceDropped;
}

// Auto-refund when conditions met
if (shouldAutoRefund()) {
    await covenant.refund(senderKeypair);
}

Why This Is Better

✅ Benefits:

⚠️ Trade-offs:

Why we accept trade-offs:

What This Enables

For senders:

For developers:

For auditors:

Testing Status


v2.3: Seller Buffer Recovery

Date: 2026-07-26
Status: ✅ Tested on chipnet
Motivation: Requirement #6 - Prevent seller capital lock

The Edge Case

Problem scenario:

t=0:     Covenant funded (payment + buffer)
t=45:    💥 Sender device crashes / offline
t=60:    ⏰ Expiry reached
         ├─ Recipient can't claim (past expiry)
         └─ Sender can't refund (device offline)
         
Result:  🔒 Seller's buffer locked forever!

Current v2.2: Only 2 functions (claim, refund). If sender offline → seller stuck.

The Solution

Add third function for seller to recover buffer after expiry:

function sellerRecoverBuffer(sig sellerSig, datasig oracleSig, bytes oracleMessage) {
    require(checkSig(sellerSig, seller));
    require(checkDataSig(oracleSig, oracleMessage, oraclePubkey));
    
    // Verify covenant has expired
    int oracleTimestamp = int(oracleMessage.split(8)[0]);
    require(oracleTimestamp >= expiryOracleTime);
    
    // Fair split (even though sender offline, sender gets payment back)
    // Output 0: Payment amount → sender address
    // Output 1: Buffer → seller address
}

Why This Is Needed

Capital efficiency:

Three recovery paths:

  1. Recipient claims (before expiry) → seller gets buffer ✅
  2. Sender refunds (anytime) → seller gets buffer ✅
  3. Seller splits (after expiry, sender offline) → seller gets buffer ✅

Same principle, different actor: Seller gets independent recovery path. Simplicity doesn’t mean fewer functions—it means each function does one clear thing.

Testing Plan

Scenario 1: Normal case (sender online)

Scenario 2: Sender offline (edge case)

Scenario 3: All offline


v2.4: Merchant Cashout (Killer Feature)

Date: 2026-07-27
Status: ✅ Tested on chipnet
Motivation: Enable in-person cash pickup at merchants

The Breakthrough

What if the recipient doesn’t have a BCH wallet? They can cash out at a local merchant instead.

The flow:

  1. Sender funds covenant (Caracas → Barcelona)
  2. Recipient sees notification: “€100 waiting at Merchant X”
  3. Recipient walks to merchant, shows QR code
  4. Merchant verifies covenant, gives €100 cash
  5. Merchant claims covenant funds (BCH payment + seller buffer)

Why this is killer:

The Fourth Function

function merchantCashout(
    sig recipientSig, 
    sig merchantSig,
    pubkey merchantPubkey,
    datasig oracleSig, 
    bytes oracleMessage
) {
    // Verify recipient approves cashout
    require(checkSig(recipientSig, recipient));
    
    // Verify merchant signature
    require(checkSig(merchantSig, merchantPubkey));
    
    // Verify oracle price + timestamp
    require(checkDataSig(oracleSig, oracleMessage, oraclePubkey));
    
    // Parse oracle data
    int oracleTimestamp = int(oracleMessage.split(8)[0]);
    int currentPriceInCents = int(oracleMessage.split(8)[1]);
    
    // Validate timestamp (not expired)
    require(oracleTimestamp < expiryOracleTime);
    
    // Calculate BCH payment (same as claim)
    int bchNeeded = eurCents * 100_000_000 / currentPriceInCents;
    int floorPrice = initialBchPriceInCents * minPricePercent / 100;
    require(currentPriceInCents >= floorPrice);
    
    // Output 0: Payment → merchant (not recipient!)
    // Output 1: Buffer → seller
}

Key Differences vs Claim

Aspect Claim Merchant Cashout
Who gets BCH Recipient Merchant
Signatures needed 1 (recipient) 2 (recipient + merchant)
Real-world flow Digital transfer Cash pickup
Use case Crypto-savvy recipient Non-crypto recipient

Security model: Recipient must explicitly approve merchant (signature required). Merchant can’t steal—needs recipient’s cooperation.

Why This Changes Everything

Before v2.4: Asgaya was a crypto-to-crypto payment rail with guaranteed value.

After v2.4: Asgaya is a fiat-to-fiat payment rail using Bitcoin Cash as settlement layer.

The implication: Recipients don’t need to understand Bitcoin Cash. They just know “I can pick up €100 at the corner store.” The covenant guarantees the merchant gets paid.

Merchant incentive:

Testing Results (Chipnet)

Validation: All 3 paths work (claim, refund, merchantCashout). Seller recovery not yet tested (needs orchestration).


v2.5: Refund Anytime (Production Ready)

Date: 2026-07-27
Status: ✅ PRODUCTION - All 4 paths tested
Motivation: Complete the design with maximum flexibility

The Final Simplification

The insight: Sender funds the infrastructure. Sender should have maximum control.

What changed: Removed ALL restrictions from refund path.

The problem with v2.4 refund: Still had conditions that trapped sender’s capital

// v2.4 approach (restrictive - this is what we moved away from)
function refund(sig senderSig, datasig oracleSig, bytes oracleMessage) {
    require(checkSig(senderSig, sender));
    
    // Parse oracle timestamp
    int oracleTimestamp = int(oracleMessage.split(8)[0]);
    
    // Require either condition
    bool oracleExpired = oracleTimestamp >= expiryOracleTime;
    bool priceDropped = currentPriceInCents < floorPrice;
    
    require(oracleExpired || priceDropped);  // ← Still restricting! Sender can't refund at t=5 if neither condition met
    
    // Refund outputs...
}

Why this was bad: If covenant expires in future but hasn’t expired yet, sender’s capital is locked even though claim is impossible. v2.5 fixes this by trusting the client layer instead.

v2.5 refund (permissionless):

function refund(sig senderSig) {
    require(checkSig(senderSig, sender));
    
    // Output 0: Payment → sender
    // Output 1: Buffer → seller
}

That’s it. No oracle. No time check. No price check. Just signature verification.

Why This Is Correct

The concern: “Won’t senders abuse this? Refund immediately after funding?”

The answer: Yes, they could. But:

  1. Social layer: Recipient sees refund on Nostr → sender reputation destroyed
  2. Economic layer: Sender loses buffer (goes to seller anyway)
  3. UX layer: App hides refund button, only auto-refunds on legitimate conditions
  4. Permissionless layer: Emergency escape if app logic fails

The philosophy: Covenant is permissionless, app is opinionated.

Analogy: Bitcoin allows anyone to send to any address (permissionless). Wallets show warnings for bad addresses (opinionated). Same pattern.

The Complete v2.5 Design

4 functions, 4 actors, 4 recovery paths:

Function Who When Result
claim Recipient + Oracle Before expiry, price OK Recipient gets BCH
merchantCashout Recipient + Merchant + Oracle Before expiry, price OK Merchant gets BCH
refund Sender Anytime Sender gets payment back
sellerRecoverBuffer Seller + Oracle After expiry, sender offline Seller gets buffer back

Capital never trapped:

Permissionless + opinionated:

Testing Results (Chipnet)

All 4 paths tested end-to-end:

Path Status TXID Notes
claim ✅ a3bbf89a... Recipient + oracle, price check passed
merchantCashout ✅ f8e4d2c3... Recipient + merchant + oracle
refund ✅ c7d9e1f2... Sender only, no oracle needed
sellerRecoverBuffer ✅ b6a8c0d4... Seller + oracle, post-expiry

Final validation: Created covenant, funded with 0.0075 BCH, successfully claimed via all 4 paths in separate tests. No funds trapped, no edge cases discovered.

Bytecode fingerprint (v2.5):

db7c643e5730713b88962d84c83626ecffbaa0e327de25bbe196a412310bc509

Artifact: price-oracle-v2.5.json (compiled July 27, 2026)

Production Readiness

✅ All paths tested
✅ Oracle integration working
✅ Price floor enforcement validated
✅ Seller buffer recovery confirmed
✅ No capital lock scenarios
✅ Bytecode frozen and fingerprinted
✅ Manual construction working (July 29, Android)

v2.5 is the production covenant. Future versions may add features (multi-oracle, reputation systems), but v2.5 is complete for Phase 0.

Oracle Architecture Note

The v2.5 covenant is oracle-agnostic. It verifies a single oracle signature via checkDataSig(oracleSig, oracleMessage, oraclePubkey) but doesn’t care where that signature comes from. The same covenant works with different oracle architectures:

The covenant doesn’t change between phases—only the oracle infrastructure evolves. This separation is intentional: covenants are immutable, oracle architecture is updateable.

Reference: Distributed Monitoring - Oracle architecture evolution


v2.6: Emergency Abort + Overlap Zone (Fund Locking Fix)

Date: 2026-08-15
Status: ✅ PRODUCTION - All 5 paths tested on testnet3
Motivation: Fix critical fund locking scenario below price floor

The Problem v2.6 Solves

Discovery: In v2.5, if price drops below the 7% floor (€930), funds can become locked:

Scenario: BCH drops to €932 (6.8% drop)

v2.5 behavior:
- claim() → REJECTED (price < floor, covenant prevents it)
- refund() → TX INVALID (math: 10,700,000 - 10,729,613 = -29,613 sats)
- Result: Sender's capital TRAPPED until price recovers or MTP expires

Why this is bad:
- Sender funded the covenant (their capital at risk)
- Price drop creates urgency (want to exit immediately)
- No permissionless exit path available
- Defeats "capital never trapped" design goal

The math problem: 7% buffer doesn’t cover 7% price drop.

Example:

Why 7% buffer doesn’t work: Price drops by 6.8% → payment cost increases by 7.29% (not 6.8%). Buffer consumption is asymmetric.

The Solution: Emergency Abort Function

v2.6 adds a 5th function:

function abort(sig senderSig, datasig oracleSig, bytes oracleMessage) {
    // 1. Verify sender signature
    require(checkSig(senderSig, sender));
    
    // 2. Verify oracle signature
    require(checkDataSig(oracleSig, oracleMessage, oraclePubkey));
    
    // 3. Parse current price
    require(oracleMessage.length == 16);
    bytes8 priceBytes = unsafe_bytes8(oracleMessage.split(8)[1]);
    int currentBchPrice = int(priceBytes);
    
    // 4. Check abort threshold (6.5% drop from initial price)
    int abortThreshold = (initialBchPriceInCents * 935) / 1000;
    require(currentBchPrice <= abortThreshold);
    
    // 5. Single output - everything to sender
    require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(hash160(sender)));
}

Key design decisions:

  1. Abort threshold: 6.5% (€935) - NOT 7% (€930)
    • Creates overlap zone (€935-€930) where both abort AND refund work
    • Prevents lock zone where neither path works
    • User choice at overlap: keep buffer (refund) or maximize recovery (abort)
  2. Single output - Critical for on-chain detection
    • abort() → 1 output (entire balance to sender)
    • claim/refund → 2 outputs (payment + remainder split)
    • Output count signals which path was taken
  3. Permissionless exit - Sender can abort when price drops ≥6.5%
    • No waiting for MTP expiry
    • No dependency on recipient cooperation
    • Emergency escape always available

The Overlap Zone Design

Genius insight: 6.5% threshold creates 0.5% overlap where BOTH paths work.

Price Range abort() refund() User Choice
€1000-€935 ❌ Rejected ✅ Works Normal refund only
€935-€930 ✅ Works ✅ Works OVERLAP: Both valid!
€930-€0 ✅ Works ❌ Math fails Emergency abort only

Why overlap zone matters:

  1. Eliminates lock zones - At every price, at least one path works
  2. User sovereignty - Sender chooses: maximize recovery (abort) or preserve seller economics (refund)
  3. Smooth transition - No cliff edge where paths flip
  4. On-chain proof - Transaction output count reveals which path taken

Testing Results (Testnet3)

Complete validation on Pi-chan (Bitcoin Core + Fulcrum):

Scenario 1: Overlap Zone (€935 - 6.5% drop)

Both paths tested:

Path Status TXID Outputs Amounts
abort() ✅ Works 693be518... 1 10,699,000 sats → sender
refund() ✅ Works 2ad2f7fa... 2 10,695,187 sats → sender
3,813 sats → seller

Proof: At €935, user can choose either path - both valid on-chain. ✅

Scenario 2: Danger Zone (€932 - 6.8% drop)

Only abort works:

Path Status TXID Error
abort() ✅ Works 9401e144... (none - single output 10,699,000 sats)
refund() ❌ Fails (not broadcast) “Tried to add output with -30,613 satoshis”

Insight: Covenant ALLOWS refund (price >= floor check passes: 93200 >= 93000), but transaction builder CATCHES negative remainder. Double protection: covenant logic AND math validation. ✅

Scenario 3: Floor (€930 - 7.0% drop)

Status: Oracle signature ready, not yet tested (abort expected to work, refund expected to fail)

The Complete v2.6 Design

5 functions, 4 actors, 5 recovery paths:

Function Who When Result Outputs
claim Recipient + Oracle Before expiry, price ≥ floor Recipient gets BCH 2
merchantCashout Recipient + Merchant + Oracle Before expiry, price ≥ floor Merchant gets BCH 2
refund Sender + Oracle Anytime, price ≥ floor Sender gets payment back 2
abort Sender + Oracle Price ≤ 93.5% of initial Sender gets everything 1
sellerRecoverBuffer Seller + Oracle After expiry, sender offline Seller gets buffer back 2

Capital never trapped (improved):

On-chain detection:

Critical Technical Detail: Oracle Signature Library

Discovery during testnet3 validation: Oracle signatures MUST use bitcoincashjs-lib crypto library, not Node.js built-in crypto.

Wrong pattern (fails checkDataSig):

import crypto from 'crypto';
const messageHash = crypto.createHash('sha256').update(message).digest();
const signature = oracleKey.sign(messageHash);

Correct pattern (passes checkDataSig):

import pkg from 'bitcoincashjs-lib';
const { ECPair, networks, crypto } = pkg;
const messageHash = crypto.sha256(message);
const signatureObj = oracleKey.sign(messageHash);
const signature = signatureObj.toDER();

Why this matters: Signature format must exactly match what CashScript covenant expects. Subtle difference in hash computation or DER encoding causes checkDataSig rejection.

Production implication: All oracle signature creation must use bitcoincashjs-lib crypto. Document this pattern to avoid expensive rediscovery.

Production Readiness

✅ All 5 paths tested (4 on chipnet v2.5, abort on testnet3 v2.6)
✅ Overlap zone validated (both abort and refund work at €935)
✅ Danger zone protected (abort saves funds at €932)
✅ Math guards confirmed (negative remainder caught before broadcast)
✅ Oracle signature pattern established (bitcoincashjs-lib crypto required)
✅ On-chain detection verified (output count discriminates paths)
✅ No capital lock scenarios (at every price, at least one path works)

Bytecode fingerprint (v2.6):

(to be computed on final compilation)

Artifact: price-oracle-v2.6.json (compiled August 15, 2026)

v2.6 is the new production covenant. It supersedes v2.5 by fixing the fund locking scenario while preserving all v2.5 functionality. The abort path enables Phase 0 H€ minting compliance (utility token, not money substitute).

v2.6.1 (Revision): Funder Semantics Rename

What: Renamed seller → funder parameter for clarity (per the funder principle).

Why at zero cost: Constructor parameter names live in the artifact ABI, not the spending bytecode. Compiling with pubkey funder produces byte-identical bytecode to v2.6 — same address, no redeployment, no re-validation.

Decision: NOT a new version (same address) — a revision. The version stays v2.6; price-oracle-v2.6.1.json is the production artifact.

Update to funder-principle doc: The “Future Considerations → Phase 1+: Parameter Naming” section deferred this rename. It is now done at zero cost in v2.6.1. The naming confusion that caused the Aug 2 and Aug 10 bugs is permanently resolved.

Abort and the Funder Principle (Teaching Moment)

In the seller-funded flow, abort() is the ONLY covenant path where the funder gets nothing from the buffer. The other 4 paths (claim, merchantCashout, refund, sellerRecoverBuffer) all return the remainder to the funder. On abort, the buffer is consumed by the price drop — there is nothing left to return.

Important distinction (self-funded flow): When the sender IS the funder (self-funded, as in Phase 0 testing), abort() sends everything to the sender — which is the funder. So in the self-funded flow the funder gets all the BCH (minus network fees incurred during funding and abort), not nothing. “The funder gets nothing” specifically means: the funder gets nothing beyond what they funded — the buffer portion is gone.

Validated on-chain (testnet3, Aug 15): Abort TXID 245ecd0a8ba8515703a4b5150766ba0fcdbbefdcf6efaaac4f5806e535dd89e7 — single output, 826,129 sats to sender, 1,000 sats fee, buffer portion consumed (sender/funder received covenant balance minus fees).

The simple outcome: The mechanics are counterintuitive (buffer consumption is asymmetric), but the result is simple — abort has one output because the buffer is gone.

Sender Offline + Deep Drop (Deliberate Design)

Question considered: In the worst case (price drop >7% AND sender offline), neither abort() (needs sender sig) nor sellerRecoverBuffer() (its split math breaks below ~6.8%) works.

Decision: Correct by design, not a gap.

Future work: The overlap between abort() and the other paths can be tightened with math refinement (dynamic buffer makes this easier — see variable-buffer-rate). Phase 0: current overlap is good enough.

H€ Minting Policy (Phase 0 Compliance)

Regulatory constraint: H€ (Hedge Euro) must remain utility token, not money substitute.

Design implication: Limit H€ minting to specific, justifiable use cases:

  1. merchantCashout() - Legitimate hedge conversion (merchant has EUR fiat, converts to H€)
  2. abort() - Emergency price protection (sender loses BCH exposure, needs EUR hedge)

On-chain detection for minting:

Compliance proof: If BCH price stabilizes, H€ becomes obsolete - proves it’s just volatility protection, not money.

Reference: Stability Layer - H€ architecture and compliance

Test Matrix Summary

Scenario Price Drop % abort() refund() Why Evidence
Normal €1000-€936 0-6.4% ❌ Rejected ✅ Works Price above threshold (covenant rejects abort)
Overlap €935 6.5% ✅ Works ✅ Works Both paths safe TXID: 693be518... (abort)
TXID: 2ad2f7fa... (refund)
Danger €934-€931 6.6-6.9% ✅ Works ❌ Math fails Only abort saves TXID: 9401e144... (abort €932)
Error: -30,613 sats (refund €932)
Floor €930 7.0% ✅ Works ❌ Math fails Exact threshold edge (signature ready, not tested)
Deep <€930 >7.0% ✅ Works ❌ Math + covenant Abort only path (signature ready, not tested)

Key insight: “Unintended feature” - refund fails not because covenant rejects it (price check passes), but because transaction math prevents it (can’t create negative output). Double protection is robust design.

Production abort success (testnet3): TXID 245ecd0a8ba8515703a4b5150766ba0fcdbbefdcf6efaaac4f5806e535dd89e7 — single output, 826,129 sats to sender, 1,000 sats fee, buffer portion consumed (self-funded test: sender = funder, received covenant balance minus fees).

Note on coverage: sellerRecoverBuffer() is the only path not yet tested on testnet3 (requires an expired covenant). It was validated on chipnet in v2.3. The other 4 paths are validated on testnet3 (claim/refund inter-device, abort Aug 15).


Evolution Summary Table

Version Problem Solved Problem Created Key Lesson
Phase 1 Basic refund path MTP too slow (hours) Chipnet needs fast paths
v2.0 Fast refund (5 min) Oracle dependency for refund Oracle ≠ always available
v2.1 Claim rejects below floor Can’t refund on price drop before expiry Covenant shouldn’t trap funds
v2.2 Simple covenant, emergency escape Two-layer mental model User sovereignty > safety theater
v2.3 Seller capital recovery Limited to 3 actors Three recovery paths = robust
v2.4 Merchant cashout (4th path) Refund still restricted Merchants enable non-crypto recipients
v2.5 Refund anytime (permissionless) (none - design complete) Covenant allows, client enforces
v2.6 Emergency abort (fund locking fix) (none - testnet3 validated) Overlap zone prevents all lock scenarios

The realization: We kept adding complexity to handle edge cases. Moving logic to the client solved most issues (v2.5). The abort function (v2.6) solves the final edge case: price drops below buffer capacity. The overlap zone design (6.5% threshold) ensures capital is never trapped at any price.


Critical Bugs Discovered

1. Byte Endianness (Phase 1)

Bug: CashScript reads bytes as little-endian, but we created oracle messages as big-endian.

Symptom: Covenant rejected claims with valid oracle signatures. Price value completely wrong.

Fix: Changed writeBigInt64BE() → writeBigInt64LE() in oracle signature creation.

Time lost: 3 hours of debugging 😅

Lesson: Always verify wire format when integrating off-chain data with on-chain logic.

2. Seller Buffer Lock (Discovered between v2.2 and v2.3)

Bug: If sender device offline after expiry, seller’s buffer permanently locked.

Symptom: Only 2 functions (claim, refund). No recovery path for seller if sender offline.

Fix: Add sellerRecoverBuffer() function (v2.3).

Lesson: Edge cases matter. Real-world devices fail. Capital efficiency requires all participants to have recovery paths.

3. Funder Parameter Semantics (Discovered August 2, 2026)

Naming confusion: The seller parameter in the covenant constructor is semantically a funder parameter.

Why this matters:

Buffer ownership semantics:

Why the parameter is named “seller”:

Production implications:

Example flows:

Remittance (seller is BCH seller):

Sender buys BCH from seller → Seller funds covenant
├─ Claim: Payment to recipient, buffer to seller ✅
└─ Refund: Payment to sender, buffer to seller ✅

Merchant payment (seller is sender):

Sender already owns BCH → Sender funds own covenant
├─ Claim: Payment to merchant, buffer to sender ✅
└─ Refund: Payment to sender, buffer to sender ✅

Discovered during: August 1-2, 2026 testnet3 validation (7 successful transactions)

Documentation status:

Future consideration: If v3.0 is needed, consider renaming seller → funder for clarity. For v2.5, parameter name is frozen (covenant deployed), but semantic understanding is now documented.


Testing Methodology

Testnet3 vs Regtest Trade-offs

Testnet3 (current production testing):

Regtest (early development):

Evolution:

Why testnet3: Reliable enough for continuous testing, real network conditions, proven stable during August 1-2 validation (7 successful transactions)

Test Parameters Used

Example covenant params (historical chipnet testing, July 2026):

{
  "actors": {
    "sender": { "pubkey": "...", "address": "bchtest:qrzlve3y..." },
    "recipient": { "pubkey": "...", "address": "bchtest:qqq5vtgu..." },
    "seller": { "pubkey": "...", "address": "bchtest:qpwgshlma..." },
    "oracle": { "pubkey": "...", "address": "bchtest:qz6..." }
  },
  "payment": {
    "eurCents": 700,        // €7.00
    "bufferSats": 49000     // 0.00049 BCH (7% buffer)
  },
  "timelock": {
    "expiryOracleTime": 1721937723,  // 5 minutes for testing
    "expiryMTP": 1721941323          // 1 hour fallback
  }
}

v2.1 price drop params:

{
  "initialBchPriceInCents": 100000,  // €1000/BCH
  "minPricePercent": 93              // 93% = 7% drop max
}

Implementation Files

Archived:

Current:

Testing scripts:


Key Decisions & Rationale

1. Why Simplify v2.2 Instead of Adding More Features?

Initial instinct: Add refundPriceDrop() function to v2.1.

Better solution: Remove all conditions from refund(), move logic to client.

Why:

Reference: Covenant Simplicity Principle

2. Why Oracle for Claim but Not Refund?

Claim requires oracle:

Refund doesn’t require oracle:

Asymmetry is intentional: Recipient has deadline, sender has flexibility.

3. Why Seller Gets Buffer in All Scenarios?

Fair compensation:

Three scenarios:

  1. Recipient claims → Seller gets buffer ✅
  2. Sender refunds → Seller gets buffer ✅
  3. Seller splits (v2.3) → Seller gets buffer ✅

Capital efficiency: Seller must be able to recover buffer in all cases, or liquidity dries up.


Lessons Learned

Technical Lessons

  1. Byte order matters - Always verify wire format (3 hours of debugging saved future devs)
  2. Test network selection matters - Testnet3 mirrors mainnet well enough for iterative testing (switched from chipnet in August 2026)
  3. Edge cases are real - Devices crash, users go offline (plan for failure)
  4. Simple covenants > complex - Moving logic to client solved multiple problems at once

Design Lessons

  1. User sovereignty matters - It’s the user’s money, covenant shouldn’t trap it
  2. Permissionless at protocol, opinionated at app - Same pattern as Bitcoin
  3. Capital efficiency drives adoption - Seller must be able to recover buffer
  4. Two-layer mental model is worth it - Simple covenant + smart client = robust system

Process Lessons

  1. Test on real network - Real testnet revealed MTP behavior and network conditions (regtest wouldn’t show this)
  2. Iterate quickly - v2.0 → v2.1 → v2.2 in one day (fast feedback loop)
  3. Archive old versions - ARCHIVE__YYYYMMDD. for reference
  4. Document as you go - This version history prevents knowledge loss

Post-v2.5 Production Milestones (August 2026)

Status: 🏆 Production-proven - First inter-device covenant claim successful
Period: August 8-10, 2026
Evidence: On-chain TXID: 193c3c9e5287e13cc56e1401aed55de34db9a375312e052807aea060e58e3d96


August 8, 2026: Covenant Lifecycle Complete

Milestone: Full covenant lifecycle proven on testnet3 (create → fund → refund)

What was validated:

Key achievement: Sender can now safely use covenants knowing the refund safety net works.

Status: Refund path production-ready


August 8-9, 2026: Self-Funding Flow Hardened

Milestone: Production-grade self-funding flow with connection management

What was implemented:

Copy-to-Share Format:

[COVENANT_V25]
covenantAddress=bchtest:p...
senderPubkey=032774f...
recipientPubkey=03886b4f...
sellerPubkey=032774f...
oraclePubkey=02f2c7e...
eurCents=500
expiryOracleTime=1786313404
initialBchPriceInCents=65000
minPricePercent=93
fundingTxid=9b98c94c...
[/COVENANT_V25]

Key insight: Off-chain parameter coordination via Telegram (or Nostr) enables cross-device covenant claims while maintaining on-chain validation.

Status: Self-funding flow production-ready


August 9, 2026: Connection Management Discovery

Issue: WebSocket operations hanging after TCP queries

Root cause discovered:

Solutions implemented:

  1. 5-second TCP cooldown after balance queries (tested: 2s insufficient)
  2. Finally blocks in all WebSocket operations (always disconnect)
  3. Port documentation for Pi-chan Fulcrum:
    • Port 60001: TCP (ElectrumClient balance queries)
    • Port 60003: WebSocket (covenant operations)
    • Port 60004: WebSocket Secure (future)

Lesson learned: Mobile connection management is architecture, not implementation detail. Document these patterns to prevent future debugging sessions.

Documentation: Connection Management Patterns


August 10, 2026: First Successful Inter-Device Claim 🎉

Milestone: 🏆 HISTORIC - First guaranteed-value BCH transfer using native covenants between two Android devices!

Setup:

Transaction Details:

TXID: 193c3c9e5287e13cc56e1401aed55de34db9a375312e052807aea060e58e3d96

Covenant funded: 827,129 sats (€5 + 7% volatility buffer at €650/BCH)

Output 0 (Recipient - Isabel):
  Amount: 0.00769230 BCH (769,230 sats)
  EUR value: €5.00 (at €650/BCH claim price)
  Address: bchtest:qq2uxg4cu9axyzd9gjnhxwrvealt44mcwunp7gzd0k ✅

Output 1 (Sender - Volatility Buffer):
  Amount: 0.00056899 BCH (56,899 sats)
  Buffer %: 7.4% (within 7% target)
  Address: bchtest:qrw5nukh5jqend8922tf8zhxwyku6wfpxu9nl79hxf ✅

Transaction fee: 1,000 sats
Total outputs: 826,129 sats (funded - fee)

Verification: bitcoin-cli -testnet gettransaction 193c3c9e...
Result: Both wallets confirmed receipt ✅

What this proves:

Status: 🎉 Core value proposition proven on-chain!


August 10, 2026: Critical Bug Discovery & Fix

Bug: 🔥 SHOW-STOPPER - All claim attempts rejected by covenant validation

What went wrong:

Initial claim implementation sent volatility buffer to recipient’s address instead of seller’s (funder’s) address:

// ❌ WRONG - Initial implementation
put("sellerAddress", recipientAddress)  // Buffer to recipient!

// ✅ CORRECT - After debugging
put("sellerAddress", sellerWallet.address)  // Buffer to seller (funder)!

Why it failed:

The covenant v2.5 claim path validates:

// Output 0: Payment to recipient ✅
require(tx.outputs[0].value == eurPayment);
require(hash160(tx.outputs[0].lockingBytecode) == recipient);

// Output 1: Buffer to SELLER (funder) ❌
require(tx.outputs[1].value >= buffer);
require(hash160(tx.outputs[1].lockingBytecode) == seller);  // ← FAILED!

Covenant rejected transaction because buffer output went to recipient address, not seller address. The smart contract was working as designed - enforcing the funder principle!

Error message (cryptic):

Error: PriceOracle.cash Error in transaction at input 0
Reason: Unsuccessful evaluation: completed with a non-truthy value

Didn’t indicate WHICH output failed or WHY. Took ~2 hours of debugging to discover the issue.

The fix:

// Find SELLER wallet (funder) by matching sellerPubkey
val sellerPubkey = remittance.sellerPubkey
val sellerWallet = walletManager.findWalletByPubkey(sellerPubkey)

// Use SELLER's address for buffer output
val txid = covenantWebView.claimCovenant(
    recipientAddress = recipientWallet.address,  // Payment
    sellerAddress = sellerWallet.address         // Buffer ✅
)

Key insight: Understanding sellerPubkey parameter semantics (August 2 discovery) was necessary but not sufficient. We also needed to use seller’s address (not just pubkey) in transaction building.

Impact: Without this fix, NO covenant could ever be claimed successfully. This was a production-blocking bug caught during end-to-end testing.

Lesson learned:

  1. End-to-end testing is essential - Unit tests wouldn’t catch this (transaction built successfully, only covenant validation failed)
  2. Smart contracts prevent errors - Covenant rejection forced us to fix the bug before shipping
  3. Documentation prevents regression - This bug WILL be reintroduced if not documented

Full documentation: Funder Principle - August 10 Bug


Architecture Components Validated

August 8-10 testing validated:

  1. NotificationListener (Android NotificationListenerService)
    • Monitors Telegram notifications
    • Parses [COVENANT_V25] blocks
    • Stores received covenants in database
  2. Database Layer (Room + Flow)
    • isReceived flag differentiates sent vs received covenants
    • Flow updates UI automatically on new covenant
  3. Wallet Matching (Critical!)
    • Find recipient wallet by recipientPubkey
    • Find SELLER wallet by sellerPubkey (funder!)
    • Extract WIF (signing) and address (outputs)
  4. Transaction Building (Kotlin ↔ JavaScript Bridge)
    • CovenantWebView orchestrates JavaScript execution
    • CashScript SDK recreates contract from parameters
    • SignatureTemplate signs with recipient’s WIF
  5. On-Chain Validation
    • Covenant verifies oracle signature (CHECKDATASIG)
    • Covenant validates payment amount (exact EUR value)
    • Covenant validates output addresses (recipient + seller)
    • Transaction broadcast only if ALL checks pass

Documentation: End-to-End Claim Flow


Production Readiness Assessment

What’s production-ready (August 10, 2026):

✅ Covenant v2.6 smart contract

✅ Self-funding sender flow

✅ Recipient claim flow

✅ Connection management

What needs work before mainnet:

⏳ Oracle price feed

⏳ Merchant cash-out flow

⏳ Multi-covenant batching

⏳ Error handling polish


Summary: From Testing to Production

Timeline:

Status transition:

Core value proposition: Guaranteed EUR-denominated payments on Bitcoin Cash using native covenants, with no custodians and full smart contract enforcement.

Evidence: On-chain TXID 193c3c9e5287e13cc56e1401aed55de34db9a375312e052807aea060e58e3d96 - recipient received exactly €5, sender received buffer back, covenant validated all outputs. It works! ✅


Future Directions

Potential v3.0: Multi-Oracle Consensus

Current v2.6: Single oracle signature verification via checkDataSig.

Oracle evolution (covenant stays the same):

Why the covenant doesn’t need to change: v2.6 verifies one signature from one pubkey. The oracle infrastructure can evolve from single-source to multi-source consensus without changing the covenant. The client determines which oracle signature to trust based on reputation, source diversity, and network consensus.

True v3.0 (if needed): Covenant-level multi-oracle (verify N signatures, calculate median on-chain). This would require more complex covenant logic and larger scripts. The trade-off (decentralization vs covenant simplicity) may not be worth it if Phase 2’s blockchain-as-oracle already provides sufficient decentralization at the client layer.

Reference: Distributed Monitoring - Oracle architecture and consensus models


Design rationale:

User experience:

Implementation:


🏠 Home ↑ Covenants 📖 Glossary
Related: Covenant Simplicity Auto-Refund UX

Status: 🏆 Production-Proven - v2.6 complete, all 5 paths tested, first inter-device claim successful
Last Milestone: August 10, 2026 - First guaranteed-value covenant claim between two devices
Evidence: TXID 193c3c9e5287e13cc56e1401aed55de34db9a375312e052807aea060e58e3d96
Updated: 2026-08-21


August 20-21, 2026: v0.2 Hybrid Architecture (Client Layer)

Milestone: All 4 covenant operations (CREATE/REFUND/CLAIM/ABORT) migrated to the v0.2 hybrid — WebView builds+signs, Kotlin broadcasts. The covenant itself is unchanged (v2.5/v2.6 bytecode identical); this is a client-architecture change.

What changed:

Why: WebView JS timers pause on screen-off → WebSocket hangs + connection accumulation (4+ ESTABLISHED) broke multi-device use. The hybrid eliminates this at the root.

Key discoveries (see webview-covenant-bridge.md):

Results: CREATE ~100ms, covenant ops ~200ms, zero WebSocket connections on the critical path, multi-device stress test passing.

Status: Covenant specification unchanged (v2.6). Client implementation upgraded to hybrid.