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 sellerfunder 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 sellerfunder 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.5 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.5: Single oracle signature verification via checkDataSig.

Oracle evolution (covenant stays the same):

Why the covenant doesn’t need to change: v2.5 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.5 complete, all 4 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.