BTC Swap Architecture
Architecture design for safe and reliable Bitcoin to CloudCoin exchanges
The Problem
Solutions
Implementation
Workflow
Recommended Approach
Python Seller Script
How It Works
Setup Instructions
The Problem
- Bitcoin transactions are public on the blockchain.
- Anyone monitoring the RAIDA wallet address can see a txid and falsely claim it as their purchase.
- We need a way to verify that the sender of the Bitcoin is the actual buyer.
- Items are unique (only one locker each exists).
- Multiple buyers might send BTC for the same item at the same time.
- Pre-locking items (e.g., reserving them before payment) risks denial-of-service attacks by trolls locking everything without paying.
Solutions
- Unique Payment Addresses per Buyer (increases privacy too)
- Refunds if the Locker is already sold
How It Works: Generate a unique Bitcoin address for each buyer or order. When they send BTC to that address, you can match the payment to their order without needing a txid or additional verification.
- Requires our system to generate and manage multiple addresses (e.g., via a wallet with HD key derivation like BIP-32).
- We need to track which address corresponds to which order.
Implementation:
- User is shown the lockers for sale in the CloudCoin Desktop.
- User clicks the Buy button in the CloudCoin Desktop
- CloudCoin Desktop generates a random locker code.
- CloudCoin Desktop sends the locker code to the "GetBuyWallet" command in the "Python Seller" script on our Bitcoin Node.
- Python Seller script records the Locker code, creates a Bitcoin Wallet and sends the address to the CloudCoin Desktop.
- CloudCoin Desktop displays and stores the new RAIDA address along with a QR code of the address.
- CloudCoin Desktop tells the user that they only have one hour to make their purchase
- CloudCoin Desktop shows a button under the QR code that says "Click Here after you have sent your payment of exactly ### Bitcoin"
- CloudCoin Desktop sends the Locker Code it generated earlier to the Python Seller script.
- Python Seller Script on the Bitcoin node checks its database and finds the BTC address based on the user's locker code.
- Python Seller Script looks at that address's transaction records and sees if the payment has been received.
- If the payment has been received, the Python Seller script calls a service on the RAIDA and tells it to create a locker with the User's locker number and move the CloudCoin from the Sales locker to the User's locker. The RAIDA responds with the seller's BTC address.
- Python Seller's script then sends BTC to the seller's address. And the fee to the RAIDA wallet.
- Python returns the success code to the CloudCoin Desktop and writes the transaction in the past sales log.
- CloudCoin Desktop downloads the locker and puts it in the wallet's transaction log.
- Another user sends a message that they have bought the same locker that was just sold.
- Python responds with an error code saying that locker has already been sold.
- Python sends the BTC back to the second buyer minus any fees that we must take out. We can call it a "Refund fee"
Workflow
- Display Item as Available:
- List the item with a unique Bitcoin address (as per the earlier "Unique Payment Addresses" solution).
- Buyers Send Payment:
- Anyone can send BTC to that address at any time—no pre-reservation required.
- First Valid Payment Wins:
- Monitor the address for incoming transactions (e.g., via a blockchain API or your node).
- The first transaction that meets your criteria (e.g., correct amount, confirmed or at least 1 confirmation) claims the item.
- Update the item's status to "Sold" immediately after validating the first payment.
- Refund Latecomers: If additional payments arrive after the item is sold, refund them to the sending addresses with a note (e.g., "Item already sold, refunded").
- Optional Secret Code: To prevent txid spoofing (from your earlier concern), pair the address with a unique secret code per listing. Buyers submit the txid + code, and you verify both.
Recommended Approach
Stick with First-Come, First-Served with Unique Addresses:
No pre-locking, so no abuse potential.
Scales well for unique items.
Refunds handle multiple buyers cleanly.
Add the secret code for extra security against txid spoofing.
Goals
The goal is to sell unique items securely, awarding the item to the first valid payer and refunding any latecomers, while avoiding pre-locking vulnerabilities. Below is a Python script tailored to your setup, using Bitcoin Core's REST API.
Assumptions
- You're running Bitcoin Core v0.21.0 or later with the REST interface enabled (-rest flag in bitcoin.conf).
- Your node is fully synced and has wallet functionality enabled.
- The REST API is accessible locally (e.g., http://localhost:8332/rest/).
- Each item has a unique Bitcoin address generated by your wallet.
- You're using a secret code per item to verify the buyer (optional but included for security).
Python Seller Script
import requests
import json
import time
from bitcoinrpc.authproxy import AuthServiceProxy # For RPC calls
from typing import Dict, Optional
# Configuration
RPC_USER = "your_rpc_username" # From bitcoin.conf
RPC_PASS = "your_rpc_password" # From bitcoin.conf
RPC_URL = "http://localhost:8332" # Default Bitcoin Core RPC port
REST_URL = "http://localhost:8332/rest" # REST API endpoint
MIN_CONFIRMATIONS = 1 # Wait for 1 confirmation (adjust as needed)
# Connect to Bitcoin Core via RPC
rpc = AuthServiceProxy(f"http://{RPC_USER}:{RPC_PASS}@{RPC_URL}")
# Simulated database (replace with real DB like SQLite)
items_db: Dict[int, Dict] = {
123: {
"name": "Cool Painting",
"address": None, # Will be generated
"secret_code": "X7K9P",
"price_sats": 1000000, # 0.01 BTC
"status": "Available",
"buyer_txid": None
}
}
# Generate a new address for an item
def generate_address(item_id: int) -> str:
address = rpc.getnewaddress("Item #{}".format(item_id), "bech32")
items_db[item_id]["address"] = address
return address
# Check for payments to an address via REST API
def check_payments(address: str, price_sats: int) -> Optional[str]:
url = f"{REST_URL}/txns/{address}.json"
while True:
try:
response = requests.get(url)
if response.status_code != 200:
print(f"Error fetching txns for {address}: {response.status_code}")
time.sleep(10)
continue
txns = response.json()
for tx in txns:
# Check outputs for matching address and amount
for vout in tx["vout"]:
if (vout.get("scriptPubKey", {}).get("addresses", [None])[0] == address and
vout["value"] * 100000000 >= price_sats and
tx.get("confirmations", 0) >= MIN_CONFIRMATIONS):
return tx["txid"]
print(f"No valid payment yet for {address}. Checking again...")
time.sleep(10) # Poll every 10 seconds
except Exception as e:
print(f"Error checking payments: {e}")
time.sleep(10)
# Refund a transaction
def refund_payment(txid: str, refund_address: str, amount_sats: int) -> str:
# Get raw transaction details
raw_tx = rpc.getrawtransaction(txid, True)
spent_amount = 0
for vout in raw_tx["vout"]:
if vout["scriptPubKey"]["addresses"][0] == items_db[item_id]["address"]:
spent_amount = int(vout["value"] * 100000000)
# Subtract fee (e.g., 1000 satoshis, adjust as needed)
refund_amount = spent_amount - 1000
if refund_amount <= 0:
raise ValueError("Refund amount too low after fees")
# Create refund transaction
refund_tx = rpc.createrawtransaction(
[{"txid": txid, "vout": 0}], # Input (simplified; adjust vout as needed)
{refund_address: refund_amount / 100000000} # Output in BTC
)
signed_tx = rpc.signrawtransactionwithwallet(refund_tx)
if not signed_tx["complete"]:
raise Exception("Failed to sign refund transaction")
refund_txid = rpc.sendrawtransaction(signed_tx["hex"])
return refund_txid
# Main seller logic
def process_sale(item_id: int):
item = items_db[item_id]
if item["status"] != "Available":
print(f"Item {item_id} already sold or unavailable.")
return
# Generate address if not already set
if not item["address"]:
item["address"] = generate_address(item_id)
print(f"Item {item_id} ({item['name']}) for sale at {item['address']} "
f"for {item['price_sats']} satoshis. Secret code: {item['secret_code']}")
# Monitor payments
txid = check_payments(item["address"], item["price_sats"])
print(f"Payment detected for {item['address']}: {txid}")
# Simulate buyer submission (in reality, this comes from a form/email)
buyer_submission = {"txid": txid, "secret_code": input("Enter secret code from buyer: ")}
# Verify payment and buyer
if (buyer_submission["txid"] == txid and
buyer_submission["secret_code"] == item["secret_code"]):
item["status"] = "Sold"
item["buyer_txid"] = txid
print(f"Item {item_id} sold! Confirmed txid: {txid}")
else:
print(f"Invalid submission for {txid}. Refunding...")
# Get sender address from tx (simplified; assumes one input)
raw_tx = rpc.getrawtransaction(txid, True)
refund_address = raw_tx["vin"][0]["prevout"]["addresses"][0]
refund_txid = refund_payment(txid, refund_address, item["price_sats"])
print(f"Refunded to {refund_address} with txid: {refund_txid}")
# Check for additional payments (latecomers)
time.sleep(60) # Give some time for late payments
late_txid = check_payments(item["address"], item["price_sats"])
if late_txid and late_txid != txid:
print(f"Late payment detected: {late_txid}. Refunding...")
raw_tx = rpc.getrawtransaction(late_txid, True)
refund_address = raw_tx["vin"][0]["prevout"]["addresses"][0]
refund_txid = refund_payment(late_txid, refund_address, item["price_sats"])
print(f"Refunded late payment to {refund_address}: {refund_txid}")
# Run the seller process
if __name__ == "__main__":
item_id = 123
process_sale(item_id)
How It Works
- Setup:
- Connects to your Bitcoin Core node via RPC for wallet actions (e.g., generating addresses, signing transactions).
- Uses the REST API to monitor transactions (faster than RPC for this purpose).
- Stores item data in a dictionary (replace with a real database like SQLite for production).
- Address Generation:
- generate_address() creates a new Bech32 address for the item using getnewaddress.
- Payment Monitoring:
- check_payments() polls the REST API (/txns/{address}) to find transactions sending the correct amount to the item's address with sufficient confirmations.
- Returns the first valid txid.
- Buyer Verification:
- Simulates receiving a buyer's submission (txid + secret code). In practice, this would come from a web form, email, or API.
- Checks if the txid and secret code match. If yes, marks the item "Sold."
- Refunds:
- refund_payment() crafts and sends a refund transaction if:
- The buyer's submission is invalid (wrong txid or code).
- A second payment arrives after the item is sold.
- Extracts the refund address from the transaction's input (simplified here; real logic may need to handle multiple inputs).
- Race Condition Handling:
- No pre-locking—item stays "Available" until a valid payment is confirmed.
- First valid payment wins; late payments are refunded automatically.
Setup Instructions
- Enable REST API:
- Edit
bitcoin.conf:
rpcuser=your_rpc_username
rpcpassword=your_rpc_password
rest=1
server=1
txindex=1 # Needed for REST API to track all txns
- Restart Bitcoin Core: bitcoind -daemon or ./bitcoin-qt.
- Install Dependencies:
pip install requests python-bitcoinrpc
- Run the Script:
- Update
RPC_USER
,RPC_PASS
, and ports if different. - Run:
python seller.py
Notes & Improvements
Implementation Considerations:
- Database: Replace
items_db
with a persistent store (e.g., SQLite) to track items across restarts. - REST API Limitations: The REST API's
/txns/{address}
endpoint isn't officially documented for all versions—test it on your node. If it fails, switch to RPC'slisttransactions
or a blockchain API likemempool.space
. - Refund Logic: The script assumes a single input/output for simplicity. For production, parse
vin
andvout
fully to handle complex transactions. - Security: Add authentication to your REST/RPC interface if exposed beyond localhost.
- Scalability: For multiple items, run
process_sale()
in threads or async tasks.