asgayapedia

Funder Principle: Buffer Ownership Follows Funding

The Constraint: Who should receive the buffer when a covenant is refunded or expires?

The Question: Should buffer ownership be determined by role (sender/recipient/merchant) or by who provided the funds?


What Constrains Us


The Decision: Buffer Goes to Funder

The principle:

Buffer ownership follows funding. Whoever funded the covenant with BCH receives the buffer back, regardless of their role in the transaction.


The Tagline

“Your BCH, your buffer. Always.”


In practice:

REMITTANCE FLOW (sender creates, BCH seller funds):
┌────────────────────────────────────────────┐
│ María (sender) creates covenant:           │
│ - Recipient: Elena                         │
│ - Amount: €100                             │
│ - Buffer: 7% (€7)                          │
│ - Funder: Isabel (BCH seller)              │
└────────────────────────────────────────────┘
                ↓
┌────────────────────────────────────────────┐
│ Isabel (BCH seller) funds covenant:        │
│ - Sends: 0.00749 BCH (€107)                │
│ - Gets back on refund: €107 (€100 + €7)    │
└────────────────────────────────────────────┘

MERCHANT PAYMENT FLOW (sender creates and funds):
┌────────────────────────────────────────────┐
│ Tourist creates covenant:                  │
│ - Recipient: Merchant                      │
│ - Amount: €50                              │
│ - Buffer: 7% (€3.50)                       │
│ - Funder: Tourist (sender)                 │
└────────────────────────────────────────────┘
                ↓
┌────────────────────────────────────────────┐
│ Tourist funds covenant:                    │
│ - Sends: 0.00374 BCH (€53.50)              │
│ - Gets back on refund: €53.50 (€50 + €3.50)│
└────────────────────────────────────────────┘

The Covenant Parameter

Covenant v2.5 parameter:

pubkey seller;  // Actually means: funder's public key (CashScript)

Why “seller” instead of “funder”?

The covenant was designed during remittance-first development (July 2026), where:

When merchant flows were validated (August 2026):

Clarification:


The Trade-off

Gain Consideration
Financial fairness (funder recovers capital) Parameter name doesn’t match all use cases
Consistent semantics (one rule, all flows) Developers must understand “seller = funder”
User sovereignty (your BCH, your control) Documentation burden (explain the naming)
No special cases (remittances = merchant = same logic) Could cause confusion during code review
Buffer protection (funder never loses excess) Future developers might expect “seller” to mean BCH seller only

Why This Design Works

1. Financial Fairness is Paramount

Scenario: María creates a €100 remittance covenant. Isabel (BCH seller) funds it with €107 BCH.

Question: If the covenant expires unclaimed, who should get the €107?

Answer: Isabel. She provided the capital. María never risked her own BCH.

Principle: Buffer ownership follows capital risk, not transaction initiation.


2. Multiple Use Cases, One Covenant

Remittance Flow:

Merchant Payment Flow:

Same covenant, same logic, different funding sources. No special cases needed.


3. User Sovereignty Alignment

Core principle: Users control funds they provide.

If buffer went to sender (always):

If buffer went to recipient (always):

If buffer goes to funder (current design):


Discovery Story (August 2, 2026)

Context: Testing merchant payment flows (tourist → merchant covenant)

Observation: Buffer was going to BCH seller (Isabel) instead of tourist (sender)

Initial assumption: “This is a bug - buffer should go to sender in merchant flows”

Investigation: Checked covenant parameters. Tourist’s pubkey hash was in… recipientPubkey field, not sellerPubkey.

Root cause: Parameter population error - test script put tourist in wrong field.

Realization: Covenant logic was correct. Buffer goes to seller parameter (the funder). In merchant flows, sender IS the funder, so sender’s pubkey should be in seller field.

Outcome:

See: Version History - Funder Parameter Semantics


🔥 CRITICAL: Production-Blocking Bug (August 10, 2026)

Status: Show-stopper bug discovered during first end-to-end claim test
Impact: NO covenant could be claimed successfully
Resolution: Fixed after 2 hours of debugging
Evidence: First successful claim TXID: 193c3c9e5287e13cc56e1401aed55de34db9a375312e052807aea060e58e3d96


The Bug: Seller Address vs Seller Pubkey

Context: Implementing recipient claim flow for first production-ready end-to-end covenant test between two Android devices (Moto G06 → Pixel 6a).

The critical confusion:

What went wrong:

// ❌ WRONG - Initial claim implementation (CovenantWebView.kt)
val params = JSONObject().apply {
    put("recipientAddress", recipientWallet.address)  // ✅ Correct
    put("sellerAddress", recipientAddress)  // ❌ WRONG! Buffer to recipient!
}

Why this was catastrophic:


Covenant Rejection (Working as Designed!)

Error message:

Error: PriceOracle.cash Error in transaction at input 0
Reason: Unsuccessful evaluation: completed with a non-truthy value 
on top of the stack. Top stack item: ""

What the covenant was checking:

// Simplified claim path validation (CashScript)
require(recipientOutput.value == eurPayment);              // ✅ Passed
require(hash160(recipientOutput.lockingBytecode) == recipient);  // ✅ Passed

require(bufferOutput.value == buffer);                     // ✅ Passed
require(hash160(bufferOutput.lockingBytecode) == seller);  // ❌ FAILED!
// Expected: seller (sender's) address
// Got: recipient's address
// Transaction rejected!

The covenant saved us! It enforced the funder principle on-chain, preventing incorrect buffer distribution.


Why This Bug Was Hard to Find

  1. Cryptic error: “Unsuccessful evaluation” doesn’t say WHICH output failed
  2. Multiple possibilities:
    • Oracle signature? ✅ (checked, was correct)
    • Price calculation? ✅ (checked, was correct)
    • UTXO state? ✅ (checked, covenant funded)
    • Port configuration? ✅ (checked, WebSocket working)
    • Output addresses? ← Found it after ~2 hours!
  3. Semantic confusion: We fixed sellerPubkey parameter but forgot about sellerAddress in transaction building

  4. Self-funded covenant context:
    • In self-funded: sender = seller = funder
    • We had THREE wallets: sender, recipient, seller
    • But seller IS sender (same wallet!)
    • Easy to confuse which address to use

The Fix

// ✅ CORRECT - After debugging (RemittanceActivity.kt + CovenantWebView.kt)

// 1. Find the SELLER wallet (funder) by pubkey
val sellerPubkey = remittance.sellerPubkey
val sellerWallet = walletManager.findWalletByPubkey(sellerPubkey)
    ?: throw Exception("Seller wallet not found")

// 2. Pass SELLER's ADDRESS for buffer output
val txid = covenantWebView.claimCovenant(
    covenantParams = covenantParams,
    oracleSig = oracleSig,
    recipientWIF = recipientWIF,
    recipientAddress = recipientWallet.address,  // Payment to recipient
    sellerAddress = sellerWallet.address,         // ✅ Buffer to seller (funder)!
    fulcrumHost = "192.168.1.100",
    fulcrumPort = 60003
)

Key insight: Just like we match recipientPubkey to find recipient wallet, we must match sellerPubkey to find seller wallet and use its address!


On-Chain Verification (First Successful Claim)

Date: August 10, 2026
TXID: 193c3c9e5287e13cc56e1401aed55de34db9a375312e052807aea060e58e3d96
Verification: Pi-chan testnet node (bitcoin-cli)

Transaction breakdown:

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

Output 0 (Recipient - Isabel):
  Amount: 0.00769230 BCH (769,230 sats)
  Address: bchtest:qq2uxg4cu9axyzd9gjnhxwrvealt44mcwunp7gzd0k
  Calculation: €5 ÷ €650 per BCH = 769,230 sats ✅

Output 1 (Seller/Sender - Volatility buffer):
  Amount: 0.00056899 BCH (56,899 sats)  
  Address: bchtest:qrw5nukh5jqend8922tf8zhxwyku6wfpxu9nl79hxf ← SENDER!
  Calculation: 827,129 - 769,230 - 1,000 (fee) = 56,899 sats ✅

Buffer percentage: 56,899 ÷ 769,230 ≈ 7.4% ✅

Verification command:

# On Pi-chan
bitcoin-cli -testnet -rpcwallet=sender gettransaction 193c3c9e5287...
# Shows BOTH outputs received by sender wallet ✅

bitcoin-cli -testnet -rpcwallet=recipient gettransaction 193c3c9e5287...
# Shows payment output received by recipient wallet ✅

Result: First guaranteed-value BCH transfer using native covenants between two Android devices! 🎉


Lessons Learned

  1. Parameter vs Address: Understanding sellerPubkey semantics ≠ using seller’s address in transaction
    • August 2: Fixed parameter population (which pubkey goes where)
    • August 10: Fixed transaction building (which address gets buffer output)
    • Both are critical!
  2. Covenant as Safety Net: The smart contract prevented shipping broken code
    • We couldn’t “accidentally” send buffer to wrong address
    • Covenant validation forced us to debug and fix
    • Design constraints enforced on-chain = production insurance
  3. End-to-End Testing is Essential: Unit tests wouldn’t have caught this
    • Transaction built successfully (no syntax errors)
    • Only covenant validation (on-chain) caught the semantic error
    • Always verify transactions on actual blockchain
  4. Documentation Prevents Regression: This bug WILL be reintroduced if not documented
    • Semantic confusion is easy (seller = funder, but which address?)
    • New developers won’t know to match sellerPubkey → sellerWallet → sellerAddress
    • This document is production insurance

Implementation Checklist (Updated)

When implementing covenant claiming or refunding:

Red flags:


Why August 2 Fix Wasn’t Enough

August 2 fix: Put correct pubkey in sellerPubkey parameter

What we missed: Claim transaction building uses SELLER, not sender!

Why refund worked but claim didn’t:

Takeaway: Parameter semantics understanding must extend to transaction building, not just parameter population.


Implementation Notes

Correct Parameter Population

Remittance covenant:

const covenantParams = {
    senderPubkey: maria.pubkey,        // Creates the covenant
    recipientPubkey: elena.pubkey,     // Claims the payment
    sellerPubkey: isabel.pubkey,       // ← Funder (BCH seller)
    oraclePubkey: oracle.pubkey,
    eurCents: 10000,                   // €100
    expiryOracleTime: now() + 8h,
    initialBchPriceInCents: 350,       // €3.50/BCH
    minPricePercent: 93                // 7% buffer
}

Merchant payment covenant:

const covenantParams = {
    senderPubkey: tourist.pubkey,      // Creates the covenant
    recipientPubkey: merchant.pubkey,  // Claims the payment
    sellerPubkey: tourist.pubkey,      // ← Funder (same as sender!)
    oraclePubkey: oracle.pubkey,
    eurCents: 5000,                    // €50
    expiryOracleTime: now() + 1h,
    initialBchPriceInCents: 350,
    minPricePercent: 93
}

Key insight: seller = sender in merchant flows (tourist funds their own covenant).


Buffer Distribution Logic

Covenant v2.5 refund function (CashScript):

function refund(
    sig senderSig,
    datasig oracleSig,
    bytes8 oracleMessage
) {
    // Verify sender signature
    require(checkSig(senderSig, sender));
    
    // Parse oracle data
    (int price, int timestamp) = parseOracleMessage(oracleMessage);
    require(checkDataSig(oracleSig, oracleMessage, oraclePubkey));
    
    // Calculate refund amounts
    int bchSatoshis = tx.inputs[0].value;
    int eurPayment = calculateEurValue(eurCents, price);
    int buffer = bchSatoshis - eurPayment;
    
    // Distribute
    require(tx.outputs[0].value == eurPayment);
    require(hash160(tx.outputs[0].lockingBytecode) == sender);
    
    require(tx.outputs[1].value == buffer);
    require(hash160(tx.outputs[1].lockingBytecode) == seller);  // ← Buffer to funder
}

Buffer always goes to seller parameter (the funder). No conditional logic based on flow type.


Alternative Approaches Considered

Option 1: Separate “Funder” Parameter

Proposal: Add explicit funderPubkey parameter, rename seller to something clearer

Pros:

Cons:

Verdict: ❌ Not worth the complexity for a naming issue


Option 2: Role-Based Buffer Logic

Proposal: Buffer goes to sender in remittances, recipient in merchant payments

Pros:

Cons:

Verdict: ❌ Violates core principles


Option 3: Keep Current Design, Improve Documentation ✅

Proposal: Document parameter semantics clearly, update test scripts

Pros:

Cons:

Verdict: ✅ Best approach - document and educate


Production Impact

Security Properties

Guarantee: Funder can always recover full capital (payment + buffer)

Protection:

No griefing: Recipient can’t steal buffer by refusing to claim


User Experience

Remittance flow (Maria → Elena via Isabel):

María creates covenant (€100)
Isabel funds with €107 BCH (seller = Isabel)
    ↓
If Elena claims:
  - Elena gets: €100
  - Isabel gets: €7 buffer back
  
If expired/unclaimed:
  - María refunds (she's sender)
  - Isabel gets: €107 (payment + buffer)

Merchant flow (Tourist → Merchant):

Tourist creates and funds covenant (€50 + €3.50 buffer)
    ↓
If merchant claims:
  - Merchant gets: €50
  - Tourist gets: €3.50 buffer back
  
If rejected/expired:
  - Tourist refunds
  - Tourist gets: €53.50 (payment + buffer)

Consistent UX: Funder always recovers capital. No surprises.


Documentation Checklist

When explaining covenant parameters:


Related principles:

Related documentation:


Future Considerations

Parameter Naming: RESOLVED in v2.6.1

Status: ✅ Done (August 15, 2026)

The sellerfunder rename was applied in price-oracle-v2.6.1.cash at zero cost — constructor parameter names live in the artifact ABI, not the spending bytecode, so the compiled output is byte-identical to v2.6 (same address, no redeployment).

Decision: The version stays v2.6 (same address); price-oracle-v2.6.1.json is the production artifact with the correct funder parameter name.

What this resolves: The naming confusion that caused the Aug 2 (parameter population) and Aug 10 (seller address) bugs. Future developers see funder, which matches the semantics.


Multi-Party Covenants

If future covenants involve multiple funders (e.g., 50/50 split funding):

Current scope: Single-funder covenants only (Phase 0-1)


Summary

The funder principle:

Key insight: It’s not about role (sender/recipient/merchant), it’s about capital. Whoever risked BCH gets the buffer back.

Your BCH, your buffer. Always. 🔐


Status: Production-proven (August 2-10, 2026)
Implementation: Covenant v2.5
Testing: 2 successful testnet3 refunds + 1 successful claim with correct buffer distribution (August 10)
First Claim TXID: 193c3c9e5287e13cc56e1401aed55de34db9a375312e052807aea060e58e3d96
Documentation: Complete - includes production bug discovery and resolution