Hash Time-Locked Contracts on XRPL
Learning Objectives
Explain the cryptographic mechanics of Hash Time-Locked Contracts
Implement XRPL Escrow transactions for HTLC functionality
Construct cross-chain atomic swaps between XRPL and other chains
Analyze the security guarantees and limitations of HTLC-based swaps
Evaluate when HTLCs are appropriate vs. other cross-chain methods
Every bridge solution we've examined requires trusting someone—validators, custodians, witnesses. What if we could swap assets across chains without trusting anyone?
HTLCs offer this promise.
- Both parties can claim their assets (successful swap), or
- Both parties get their original assets back (failed/expired swap)
No third party can steal funds. No bridge can be exploited. The security comes from mathematics, not economics or reputation.
But there are tradeoffs. HTLCs require both parties to be online, have timing constraints, and don't scale well for complex multi-party scenarios. Understanding both the power and limitations is essential.
What is an HTLC?
HASH TIME-LOCKED CONTRACT
Components:
├── HASH LOCK: Cryptographic puzzle (hash preimage)
│ └── Funds locked until someone provides the secret
│
├── TIME LOCK: Expiration deadline
│ └── If unclaimed by deadline, funds return to sender
│
└── CONDITIONAL RELEASE: Either claim with secret OR timeout
Key Properties:
├── Atomic: All-or-nothing execution
├── Trustless: No third party needed
├── Self-enforcing: Protocol guarantees outcomes
└── Reversible: Timeout returns funds if swap fails
The Cryptographic Puzzle:
HASH LOCK MECHANICS
1. Alice generates random secret (preimage): S = "secret123..."
2. Alice computes hash: H = SHA256(S) = "a1b2c3d4..."
3. Alice shares ONLY the hash H, keeps secret S private
1. Funds locked with condition: "Release if someone provides X where SHA256(X) = H"
1. Only someone who knows S can satisfy SHA256(S) = H
2. Once S is revealed to claim funds, it's public
3. Other party can use revealed S to claim their side
Security:
├── Hash is one-way: Can't derive S from H
├── Hash is collision-resistant: Can't find different S' where SHA256(S') = H
├── Preimage must match exactly
└── Based on SHA256 security (cryptographically secure)
Two-Party Cross-Chain Swap:
ATOMIC SWAP: ALICE (XRP) ↔ BOB (ETH)
Alice has: 1000 XRP on XRPL
Bob has: 0.5 ETH on Ethereum
They want to swap trustlessly.
PROTOCOL:
Phase 1: Setup
┌─────────────────────────────────────────────────────────────┐
│ 1. Alice generates secret S │
│ 2. Alice computes H = SHA256(S) │
│ 3. Alice shares H with Bob │
└─────────────────────────────────────────────────────────────┘
Phase 2: Lock (Alice goes first)
┌─────────────────────────────────────────────────────────────┐
│ 4. Alice creates HTLC on XRPL: │
│ - Lock: 1000 XRP │
│ - Hash: H │
│ - Timeout: 48 hours │
│ - Recipient: Bob │
│ │
│ 5. Bob verifies Alice's HTLC on XRPL │
└─────────────────────────────────────────────────────────────┘
Phase 3: Counter-Lock (Bob responds)
┌─────────────────────────────────────────────────────────────┐
│ 6. Bob creates HTLC on Ethereum: │
│ - Lock: 0.5 ETH │
│ - Hash: H (same hash!) │
│ - Timeout: 24 hours (SHORTER than Alice's) │
│ - Recipient: Alice │
│ │
│ 7. Alice verifies Bob's HTLC on Ethereum │
└─────────────────────────────────────────────────────────────┘
Phase 4: Claim (Alice reveals secret)
┌─────────────────────────────────────────────────────────────┐
│ 8. Alice claims Bob's ETH by revealing S │
│ - Alice submits S to Ethereum HTLC │
│ - Contract verifies SHA256(S) = H │
│ - Alice receives 0.5 ETH │
│ - S is now PUBLIC on Ethereum blockchain │
└─────────────────────────────────────────────────────────────┘
Phase 5: Counter-Claim (Bob uses revealed secret)
┌─────────────────────────────────────────────────────────────┐
│ 9. Bob sees S revealed on Ethereum │
│ 10. Bob claims Alice's XRP using same S │
│ - Bob submits S to XRPL Escrow │
│ - Escrow verifies hash condition │
│ - Bob receives 1000 XRP │
└─────────────────────────────────────────────────────────────┘
RESULT: Trustless swap completed!
Alice: Had 1000 XRP → Now has 0.5 ETH
Bob: Had 0.5 ETH → Now has 1000 XRP
```
The timing is critical:
TIMEOUT STRUCTURE
Alice's HTLC (XRPL): 48 hours
Bob's HTLC (Ethereum): 24 hours
WHY BOB'S TIMEOUT IS SHORTER:
- Alice creates HTLC with 24h timeout
- Bob creates HTLC with 24h timeout
- Alice waits until hour 23
- Alice claims Bob's ETH, revealing S
- Bob tries to claim XRP but Alice's HTLC expired!
- Alice gets BOTH the ETH AND her XRP back
ATTACK PREVENTED:
By making Bob's timeout shorter, Alice must reveal S
with enough time for Bob to claim before Alice's HTLC expires.
SAFE TIMING:
├── First HTLC: T hours
├── Second HTLC: T/2 hours (or less)
├── Buffer for blockchain confirmation times
└── Account for potential network congestion
---
Native HTLC Support:
XRPL has built-in Escrow functionality that supports both time-based and crypto-condition-based releases—perfect for HTLCs.
XRPL ESCROW FEATURES
Transaction Types:
├── EscrowCreate: Lock XRP with conditions
├── EscrowFinish: Claim XRP by meeting conditions
└── EscrowCancel: Return XRP after timeout
Condition Types:
├── Time-based: FinishAfter, CancelAfter
├── Crypto-condition: Condition + Fulfillment
└── Combined: Both time AND crypto conditions
Crypto-Condition Standard:
├── Uses IETF crypto-conditions (RFC)
├── Supports PREIMAGE-SHA-256
├── Condition = encoded hash
├── Fulfillment = encoded preimage
└── Standard format for interoperability
EscrowCreate Transaction:
// Create HTLC on XRPL using xrpl.js
const xrpl = require('xrpl');
const crypto = require('crypto');
const cc = require('five-bells-condition');
async function createHTLC() {
// Connect to XRPL
const client = new xrpl.Client('wss://s1.ripple.com');
await client.connect();
// Alice's wallet
const alice = xrpl.Wallet.fromSeed('sAliceSecret...');
// Generate secret and condition
const secret = crypto.randomBytes(32);
const preimageCondition = new cc.PreimageSha256();
preimageCondition.setPreimage(secret);
const condition = preimageCondition
.getConditionBinary()
.toString('hex')
.toUpperCase();
const fulfillment = preimageCondition
.serializeBinary()
.toString('hex')
.toUpperCase();
console.log('Secret (keep private):', secret.toString('hex'));
console.log('Condition (share with Bob):', condition);
console.log('Fulfillment (to claim):', fulfillment);
// Calculate timeouts
const now = Math.floor(Date.now() / 1000);
const xrplEpoch = 946684800; // XRPL epoch offset
const cancelAfter = now - xrplEpoch + (48 * 60 * 60); // 48 hours
const finishAfter = now - xrplEpoch + (60); // Can finish after 1 minute
// Create Escrow
const escrowCreate = {
TransactionType: 'EscrowCreate',
Account: alice.address,
Amount: xrpl.xrpToDrops('1000'), // 1000 XRP
Destination: 'rBobAddress...', // Bob's XRPL address
Condition: condition,
CancelAfter: cancelAfter,
FinishAfter: finishAfter
};
const prepared = await client.autofill(escrowCreate);
const signed = alice.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
console.log('Escrow created!');
console.log('Transaction:', result.result.hash);
console.log('Sequence:', prepared.Sequence); // Needed for claiming
await client.disconnect();
return {
condition,
fulfillment,
secret: secret.toString('hex'),
sequence: prepared.Sequence,
owner: alice.address
};
}
EscrowFinish Transaction:
// Bob claims the escrow using the revealed fulfillment
async function claimHTLC(owner, sequence, fulfillment, condition) {
const client = new xrpl.Client('wss://s1.ripple.com');
await client.connect();
// Bob's wallet
const bob = xrpl.Wallet.fromSeed('sBobSecret...');
// Finish (claim) the Escrow
const escrowFinish = {
TransactionType: 'EscrowFinish',
Account: bob.address,
Owner: owner, // Alice's address (escrow creator)
OfferSequence: sequence, // From EscrowCreate
Condition: condition,
Fulfillment: fulfillment // The revealed secret
};
const prepared = await client.autofill(escrowFinish);
const signed = bob.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
if (result.result.meta.TransactionResult === 'tesSUCCESS') {
console.log('Escrow claimed successfully!');
console.log('Bob received 1000 XRP');
} else {
console.log('Claim failed:', result.result.meta.TransactionResult);
}
await client.disconnect();
}
EscrowCancel Transaction:
// Cancel expired escrow (return funds to Alice)
async function cancelExpiredHTLC(owner, sequence) {
const client = new xrpl.Client('wss://s1.ripple.com');
await client.connect();
// Anyone can cancel an expired escrow
// but funds always return to original owner
const anyWallet = xrpl.Wallet.fromSeed('sAnySecret...');
const escrowCancel = {
TransactionType: 'EscrowCancel',
Account: anyWallet.address,
Owner: owner,
OfferSequence: sequence
};
const prepared = await client.autofill(escrowCancel);
const signed = anyWallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
console.log('Escrow cancelled, funds returned to:', owner);
await client.disconnect();
}
Complete Implementation:
// atomic-swap.js - Full cross-chain atomic swap
const xrpl = require('xrpl');
const { ethers } = require('ethers');
const crypto = require('crypto');
const cc = require('five-bells-condition');
// Ethereum HTLC Contract ABI (simplified)
const HTLC_ABI = [
"function newContract(bytes32 _hashlock, uint256 _timelock, address _receiver) payable returns (bytes32 contractId)",
"function withdraw(bytes32 _contractId, bytes32 _preimage) returns (bool)",
"function refund(bytes32 _contractId) returns (bool)",
"function getContract(bytes32 _contractId) view returns (address sender, address receiver, uint256 amount, bytes32 hashlock, uint256 timelock, bool withdrawn, bool refunded, bytes32 preimage)",
"event HTLCERC20New(bytes32 indexed contractId, address indexed sender, address indexed receiver, uint256 amount, bytes32 hashlock, uint256 timelock)",
"event HTLCERC20Withdraw(bytes32 indexed contractId)",
"event HTLCERC20Refund(bytes32 indexed contractId)"
];
class AtomicSwap {
constructor(config) {
this.xrplClient = null;
this.ethProvider = new ethers.JsonRpcProvider(config.ethRpcUrl);
this.htlcContract = new ethers.Contract(
config.htlcAddress,
HTLC_ABI,
this.ethProvider
);
}
async connect() {
this.xrplClient = new xrpl.Client('wss://s1.ripple.com');
await this.xrplClient.connect();
}
async disconnect() {
await this.xrplClient.disconnect();
}
/**
* Generate secret and hash for HTLC
*/
generateSecretHash() {
// Generate 32-byte random secret
const secret = crypto.randomBytes(32);
// Create hash using SHA256
const hash = crypto.createHash('sha256').update(secret).digest();
// Create crypto-condition format for XRPL
const preimageCondition = new cc.PreimageSha256();
preimageCondition.setPreimage(secret);
return {
secret: secret,
secretHex: '0x' + secret.toString('hex'),
hashHex: '0x' + hash.toString('hex'),
xrplCondition: preimageCondition.getConditionBinary().toString('hex').toUpperCase(),
xrplFulfillment: preimageCondition.serializeBinary().toString('hex').toUpperCase()
};
}
/**
* Alice: Create HTLC on XRPL (Step 1)
*/
async createXrplHtlc(aliceWallet, bobXrplAddress, amountXrp, secretHash, timeoutHours) {
const now = Math.floor(Date.now() / 1000);
const xrplEpoch = 946684800;
const escrowCreate = {
TransactionType: 'EscrowCreate',
Account: aliceWallet.address,
Amount: xrpl.xrpToDrops(amountXrp),
Destination: bobXrplAddress,
Condition: secretHash.xrplCondition,
CancelAfter: now - xrplEpoch + (timeoutHours * 60 * 60),
FinishAfter: now - xrplEpoch + 60 // 1 minute minimum
};
const prepared = await this.xrplClient.autofill(escrowCreate);
const signed = aliceWallet.sign(prepared);
const result = await this.xrplClient.submitAndWait(signed.tx_blob);
return {
success: result.result.meta.TransactionResult === 'tesSUCCESS',
txHash: result.result.hash,
sequence: prepared.Sequence,
owner: aliceWallet.address
};
}
/**
* Bob: Verify Alice's XRPL HTLC (Step 2)
*/
async verifyXrplHtlc(owner, sequence, expectedAmount, expectedHash, expectedDestination) {
// Get escrow details from XRPL
const escrowInfo = await this.xrplClient.request({
command: 'account_objects',
account: owner,
type: 'escrow'
});
// Find the specific escrow
const escrow = escrowInfo.result.account_objects.find(
obj => obj.Sequence === sequence
);
if (!escrow) {
return { valid: false, reason: 'Escrow not found' };
}
// Verify all conditions
const checks = {
amount: escrow.Amount === xrpl.xrpToDrops(expectedAmount),
destination: escrow.Destination === expectedDestination,
condition: escrow.Condition === expectedHash,
notExpired: escrow.CancelAfter > (Math.floor(Date.now() / 1000) - 946684800)
};
const valid = Object.values(checks).every(v => v);
return {
valid,
checks,
escrow
};
}
/**
* Bob: Create counter-HTLC on Ethereum (Step 3)
*/
async createEthHtlc(bobSigner, aliceEthAddress, amountEth, hashHex, timeoutHours) {
const htlcWithSigner = this.htlcContract.connect(bobSigner);
const timelock = Math.floor(Date.now() / 1000) + (timeoutHours * 60 * 60);
const tx = await htlcWithSigner.newContract(
hashHex,
timelock,
aliceEthAddress,
{ value: ethers.parseEther(amountEth.toString()) }
);
const receipt = await tx.wait();
// Extract contractId from event
const event = receipt.logs.find(
log => log.topics[0] === ethers.id("HTLCERC20New(bytes32,address,address,uint256,bytes32,uint256)")
);
const contractId = event.topics[1];
return {
success: receipt.status === 1,
txHash: receipt.hash,
contractId
};
}
/**
* Alice: Claim ETH by revealing secret (Step 4)
*/
async claimEthHtlc(aliceSigner, contractId, secretHex) {
const htlcWithSigner = this.htlcContract.connect(aliceSigner);
const tx = await htlcWithSigner.withdraw(contractId, secretHex);
const receipt = await tx.wait();
return {
success: receipt.status === 1,
txHash: receipt.hash
};
}
/**
* Bob: Extract revealed secret from Ethereum (Step 5)
*/
async extractRevealedSecret(contractId) {
const contract = await this.htlcContract.getContract(contractId);
if (contract.withdrawn) {
return {
revealed: true,
preimage: contract.preimage
};
}
return { revealed: false };
}
/**
* Bob: Claim XRP using revealed secret (Step 6)
*/
async claimXrplHtlc(bobWallet, owner, sequence, condition, fulfillment) {
const escrowFinish = {
TransactionType: 'EscrowFinish',
Account: bobWallet.address,
Owner: owner,
OfferSequence: sequence,
Condition: condition,
Fulfillment: fulfillment
};
const prepared = await this.xrplClient.autofill(escrowFinish);
const signed = bobWallet.sign(prepared);
const result = await this.xrplClient.submitAndWait(signed.tx_blob);
return {
success: result.result.meta.TransactionResult === 'tesSUCCESS',
txHash: result.result.hash
};
}
}
// Usage example
async function executeAtomicSwap() {
const swap = new AtomicSwap({
ethRpcUrl: 'https://mainnet.infura.io/v3/YOUR_KEY',
htlcAddress: '0x...' // Deployed HTLC contract
});
await swap.connect();
// 1. Alice generates secret
const secretHash = swap.generateSecretHash();
console.log('Secret generated (Alice keeps private)');
// 2. Alice creates XRPL HTLC
const aliceWallet = xrpl.Wallet.fromSeed('sAlice...');
const xrplHtlc = await swap.createXrplHtlc(
aliceWallet,
'rBobXrplAddress',
1000, // 1000 XRP
secretHash,
48 // 48 hour timeout
);
console.log('Alice created XRPL HTLC:', xrplHtlc.txHash);
// 3. Bob verifies and creates Ethereum HTLC
// (Bob would do this after verifying Alice's HTLC)
const bobEthWallet = new ethers.Wallet('0xBobPrivateKey', swap.ethProvider);
const ethHtlc = await swap.createEthHtlc(
bobEthWallet,
'0xAliceEthAddress',
0.5, // 0.5 ETH
secretHash.hashHex,
24 // 24 hour timeout (shorter!)
);
console.log('Bob created Ethereum HTLC:', ethHtlc.txHash);
// 4. Alice claims ETH (reveals secret)
const aliceEthWallet = new ethers.Wallet('0xAlicePrivateKey', swap.ethProvider);
const ethClaim = await swap.claimEthHtlc(
aliceEthWallet,
ethHtlc.contractId,
secretHash.secretHex
);
console.log('Alice claimed ETH:', ethClaim.txHash);
// 5. Bob extracts revealed secret and claims XRP
const revealed = await swap.extractRevealedSecret(ethHtlc.contractId);
const bobWallet = xrpl.Wallet.fromSeed('sBob...');
const xrpClaim = await swap.claimXrplHtlc(
bobWallet,
xrplHtlc.owner,
xrplHtlc.sequence,
secretHash.xrplCondition,
secretHash.xrplFulfillment
);
console.log('Bob claimed XRP:', xrpClaim.txHash);
console.log('Atomic swap completed!');
await swap.disconnect();
}
Solidity Implementation:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title HashedTimeLockContract
* @dev HTLC for atomic swaps with XRPL
*/
contract HashedTimeLockContract {
struct LockContract {
address sender;
address receiver;
uint256 amount;
bytes32 hashlock;
uint256 timelock;
bool withdrawn;
bool refunded;
bytes32 preimage;
}
mapping(bytes32 => LockContract) public contracts;
event HTLCERC20New(
bytes32 indexed contractId,
address indexed sender,
address indexed receiver,
uint256 amount,
bytes32 hashlock,
uint256 timelock
);
event HTLCERC20Withdraw(bytes32 indexed contractId);
event HTLCERC20Refund(bytes32 indexed contractId);
modifier contractExists(bytes32 _contractId) {
require(contracts[_contractId].sender != address(0), "Contract does not exist");
_;
}
modifier hashlockMatches(bytes32 _contractId, bytes32 _preimage) {
require(
contracts[_contractId].hashlock == sha256(abi.encodePacked(_preimage)),
"Hashlock mismatch"
);
_;
}
modifier withdrawable(bytes32 _contractId) {
require(!contracts[_contractId].withdrawn, "Already withdrawn");
require(!contracts[_contractId].refunded, "Already refunded");
_;
}
modifier refundable(bytes32 _contractId) {
require(!contracts[_contractId].withdrawn, "Already withdrawn");
require(!contracts[_contractId].refunded, "Already refunded");
require(
block.timestamp >= contracts[_contractId].timelock,
"Timelock not yet passed"
);
_;
}
/**
* @dev Create new HTLC
*/
function newContract(
bytes32 _hashlock,
uint256 _timelock,
address _receiver
) external payable returns (bytes32 contractId) {
require(msg.value > 0, "Amount must be > 0");
require(_timelock > block.timestamp, "Timelock must be in future");
require(_receiver != address(0), "Invalid receiver");
contractId = keccak256(
abi.encodePacked(
msg.sender,
_receiver,
msg.value,
_hashlock,
_timelock
)
);
require(contracts[contractId].sender == address(0), "Contract already exists");
contracts[contractId] = LockContract({
sender: msg.sender,
receiver: _receiver,
amount: msg.value,
hashlock: _hashlock,
timelock: _timelock,
withdrawn: false,
refunded: false,
preimage: bytes32(0)
});
emit HTLCERC20New(
contractId,
msg.sender,
_receiver,
msg.value,
_hashlock,
_timelock
);
}
/**
* @dev Withdraw funds by providing preimage
*/
function withdraw(bytes32 _contractId, bytes32 _preimage)
external
contractExists(_contractId)
hashlockMatches(_contractId, _preimage)
withdrawable(_contractId)
returns (bool)
{
LockContract storage c = contracts[_contractId];
c.preimage = _preimage;
c.withdrawn = true;
payable(c.receiver).transfer(c.amount);
emit HTLCERC20Withdraw(_contractId);
return true;
}
/**
* @dev Refund after timelock expires
*/
function refund(bytes32 _contractId)
external
contractExists(_contractId)
refundable(_contractId)
returns (bool)
{
LockContract storage c = contracts[_contractId];
c.refunded = true;
payable(c.sender).transfer(c.amount);
emit HTLCERC20Refund(_contractId);
return true;
}
/**
* @dev Get contract details
*/
function getContract(bytes32 _contractId)
external
view
returns (
address sender,
address receiver,
uint256 amount,
bytes32 hashlock,
uint256 timelock,
bool withdrawn,
bool refunded,
bytes32 preimage
)
{
LockContract storage c = contracts[_contractId];
return (
c.sender,
c.receiver,
c.amount,
c.hashlock,
c.timelock,
c.withdrawn,
c.refunded,
c.preimage
);
}
}
HTLC SECURITY PROPERTIES
✓ ATOMIC EXECUTION
Either both parties get funds, or neither does.
No partial execution possible.
Enforced by cryptography + timeouts.
✓ NO TRUSTED THIRD PARTY
No bridge, custodian, or validator needed.
Security from mathematics, not reputation.
Self-custodial throughout.
✓ NO COUNTERPARTY RISK
Funds locked in contracts, not sent to anyone.
Worst case: timeout and get original funds back.
Cannot lose funds to dishonest counterparty.
✓ VERIFIABLE
Both parties can verify HTLCs before proceeding.
On-chain data is transparent.
No hidden conditions.
```
HTLC ATTACK ANALYSIS
ATTACK: Timing manipulation
─────────────────────────────────────────────────────────────────
Description: Alice waits until Bob's HTLC almost expires,
then claims, leaving Bob no time.
Mitigation: Bob's timeout must be significantly shorter.
Build in buffer for network congestion.
Monitor and claim immediately when secret revealed.
ATTACK: Front-running
─────────────────────────────────────────────────────────────────
Description: Miner/validator sees Alice's claim transaction,
extracts secret, uses it before Alice's tx confirms.
Mitigation: On XRPL, less of an issue (no mempool MEV).
On Ethereum, use private mempools (Flashbots).
Account for this in timeout calculations.
ATTACK: Hash collision
─────────────────────────────────────────────────────────────────
Description: Find different preimage that produces same hash.
Mitigation: Use strong hash function (SHA256).
256-bit collision resistance is sufficient.
No practical attack known.
ATTACK: Secret prediction
─────────────────────────────────────────────────────────────────
Description: Guess or predict Alice's secret.
Mitigation: Use cryptographically secure random generation.
32 bytes = 256 bits of entropy.
Infeasible to brute force.
ATTACK: Griefing
─────────────────────────────────────────────────────────────────
Description: Alice creates HTLC, Bob responds, Alice never claims.
Bob's funds locked until timeout.
Mitigation: Reputation systems for repeated swappers.
Shorter timeouts (accept tradeoff).
Only swap with known counterparties.
```
HTLC LIMITATIONS
- ONLINE REQUIREMENT
- TIMING CONSTRAINTS
- NO PARTIAL FILLS
- COUNTERPARTY DISCOVERY
- SCALABILITY
- FINALITY REQUIREMENTS
HTLC USE CASES
GOOD FIT:
├── Large value swaps where trust is critical
├── Swaps with unknown counterparties (no reputation)
├── Technical users comfortable with complexity
├── Situations where bridges are unavailable/unsuitable
├── Privacy-conscious swaps (no third party sees)
└── Programmatic/automated swap systems
POOR FIT:
├── Small retail transactions (overhead too high)
├── Non-technical users (complexity too high)
├── High-frequency trading (too slow)
├── Situations requiring partial fills
├── When counterparty discovery is difficult
└── Casual, occasional swaps
```
HTLC vs BRIDGE COMPARISON
Dimension HTLC Bridge (e.g., Axelar)
────────────────────────────────────────────────────────────────────
Trust model Trustless (crypto) Trust validators/custodians
Third party None Yes (bridge operators)
Speed Slow (coordination) Fast (minutes)
Ease of use Complex Simple
Counterparty Need to find Swap with liquidity pool
Partial fills No Yes (via DEX)
Gas costs Multiple txs both chains Single bridge tx
Capital efficiency Funds locked during swap Liquidity pools
Scalability Limited Good
Failure mode Timeout (refund) Bridge exploit possible
BOTTOM LINE:
HTLCs are theoretically superior (trustless) but practically
inferior for most use cases. Bridges win on convenience.
HTLCs make sense for large, high-stakes swaps where trust matters.
```
PRODUCTION HTLC CONSIDERATIONS
AUTOMATION REQUIREMENTS:
├── Monitoring service for both chains
├── Automatic claim when secret revealed
├── Timeout tracking and alerts
├── Transaction retry logic
└── Secure key management
OPERATIONAL CONCERNS:
├── Gas price management (avoid stuck txs)
├── Chain congestion handling
├── Failure recovery procedures
├── Logging and audit trails
└── Alerting for anomalies
SECURITY MEASURES:
├── Hardware security modules for keys
├── Multi-sig for large amounts
├── Rate limiting
├── Anomaly detection
└── Regular security audits
```
HTLCs are elegant and trustless—the gold standard for cross-chain swaps from a security perspective. However, their complexity and coordination requirements make them impractical for most users and use cases. They're best suited for large, high-stakes swaps where the overhead is justified, or for automated systems that can handle the coordination. For everyday use, bridges (despite their trust requirements) offer a better user experience. HTLCs are important to understand but shouldn't be the default recommendation.
Assignment: Build a complete atomic swap system between XRPL and an EVM chain.
Requirements:
Implement Escrow creation with crypto-conditions
Implement Escrow claim with fulfillment
Implement Escrow cancellation
Test on XRPL testnet
Deploy HTLC smart contract (provided or modified)
Implement contract creation, withdrawal, refund
Test on Goerli/Sepolia testnet
Build monitoring for both chains
Implement automatic secret extraction
Implement automatic claim upon secret reveal
Handle timeouts and refunds
Simple UI for swap initiation
Status display for ongoing swaps
Error handling and recovery options
Security analysis of your implementation
Timeout recommendations with rationale
Operational procedures document
Known limitations and risks
Technical correctness (30%)
Security considerations (25%)
Code quality (20%)
Documentation (15%)
Testing coverage (10%)
Time investment: 8-12 hours
Value: Deep understanding of trustless cross-chain mechanics.
Knowledge Check
Question 1 of 5(Tests Knowledge):
- Original atomic swap paper (Tier Nolan, 2013)
- Bitcoin Wiki: Atomic cross-chain trading
- "Atomic Swaps" technical explainers
- XRPL Escrow documentation
- Crypto-conditions (IETF RFC)
- five-bells-condition library
- Submarine Swaps (Lightning)
- Komodo AtomicDEX
- THORChain (different model but related)
- "On the Security of Two-Party Atomic Swaps"
- MEV and front-running research
- Timing attack analysis papers
For Next Lesson:
Prepare for Lesson 13 on the Interledger Protocol, which offers a different approach to cross-chain/cross-network payments.
End of Lesson 12
Total words: ~6,500
Estimated completion time: 50 minutes reading + 8-12 hours for deliverable
Key Takeaways
HTLCs are trustless:
Use cryptographic puzzles and time locks to enable atomic swaps without any third party. Either both parties succeed or both get refunds.
XRPL has native support:
Escrow transactions with crypto-conditions provide built-in HTLC capability. No smart contracts needed on the XRPL side.
Timing is critical:
Second HTLC must have shorter timeout than first. Buffer for network delays. Timing attacks are the main vulnerability.
Complexity limits adoption:
Multiple transactions, online requirements, and coordination overhead make HTLCs impractical for casual users. Better for automated systems.
Use cases are specific:
HTLCs make sense for large value swaps, trustless requirements, and programmatic systems. Bridges are better for convenience. ---