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?
The principle:
Buffer ownership follows funding. Whoever funded the covenant with BCH receives the buffer back, regardless of their role in the transaction.
“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)│
└────────────────────────────────────────────┘
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:
seller parameter = public key hash of whoever funded the covenant| 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 |
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.
Remittance Flow:
Merchant Payment Flow:
Same covenant, same logic, different funding sources. No special cases needed.
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):
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:
seller field, regardless of role)See: Version History - Funder Parameter Semantics
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
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:
sellerPubkey parameter semantics (seller = funder)sellerPubkey fieldWhat 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:
sellerPubkey in covenant parameters (August 2 lesson learned)sellerPubkeyError 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.
Semantic confusion: We fixed sellerPubkey parameter but forgot about sellerAddress in transaction building
// ✅ 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!
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! 🎉
sellerPubkey semantics ≠ using seller’s address in transaction
When implementing covenant claiming or refunding:
sellerPubkey parameter: Whose pubkey is in the covenant?walletManager.findWalletByPubkey(sellerPubkey)sellerWallet.address (not sender’s, not recipient’s!)sellerAddress = sellerWallet.addressRed flags:
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.
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).
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.
Proposal: Add explicit funderPubkey parameter, rename seller to something clearer
Pros:
Cons:
Verdict: ❌ Not worth the complexity for a naming issue
Proposal: Buffer goes to sender in remittances, recipient in merchant payments
Pros:
Cons:
Verdict: ❌ Violates core principles
Proposal: Document parameter semantics clearly, update test scripts
Pros:
Cons:
Verdict: ✅ Best approach - document and educate
Guarantee: Funder can always recover full capital (payment + buffer)
Protection:
No griefing: Recipient can’t steal buffer by refusing to claim
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.
When explaining covenant parameters:
seller = funder (not always BCH seller)Related principles:
Related documentation:
Status: ✅ Done (August 15, 2026)
The seller → funder 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.
If future covenants involve multiple funders (e.g., 50/50 split funding):
Current scope: Single-funder covenants only (Phase 0-1)
The funder principle:
seller = funder’s pubkey hashKey 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