Libbitcoin: Cross-Chain Swaps With HTLCs

Libbitcoin: Cross-Chain Swaps With HTLCs

Cross-Chain Trades

Cross-Chain Atomic Swaps are trustless on-chain cryptocurrency trades enabled using advanced scripting features like hashed timeLock contracts(HTLC). Like hashlock contracts discussed here, HTLCs use a secret pre-image and hash to arbitrate a contract but include a timelock on the portion of the contract which allows the funds to be refunded by a private key.

Say Alice wants to trade Bob 1 tBTC for 10 tLTC:

Alice would create a contract script like this:

OP_IF OP_DUP OP_HASH160 [BOB PUBKEYHASH] OP_EQUALVERIFY OP_CHECKSIGVERIFY OP_HASH160 [HASH SECRET] OP_EQUAL OP_ELSE [nLocktime] OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 [Alice PUBKEYHASH] OP_EQUALVERIFY OP_CHECKSIG

This script says:

“The money in this contract can be spent by Bob’s private key and the Secret Hash’s pre-image or can be spent after N time by Alice’s private key.”

Bob would then create a litecoin transaction script for his side of the contract.

OP_IF OP_DUP OP_HASH160 [ALICE PUBKEYHASH] OP_EQUALVERIFY OP_CHECKSIGVERIFY OP_HASH160 [HASH SECRET] OP_EQUAL OP_ELSE [n/2 Locktime] OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 [BOB PUBKEYHASH] OP_EQUALVERIFY OP_CHECKSIG

Both of these contracts lock the coins sent to them with the hash but allow for the funds to be refunded after a certain locktime. Note, the litecoin lock time is half as much as the bitcoin locktime, this is to prevent Alice from trying to create a race condition by claiming the ltc coins and attempting to also claim the refund.

These scripts can then be hashed to addresses.

Alice then pays her 1tBTC to the contract’s address while Bob pays 10 tLTC to the LTC contract’s address.

Now, Alice can claim the 10 tLTC from the litecoin blockchain by revealing the hash pre-image in the unlocking script of the withdraw transaction.

Bob, can see the revealed pre-image and use it to redeem his 1tBTC from the other contract before the locktime runs out.

At any point in this transaction, if one side doesn’t hold up their end of the bargain the other side can simply refund their money with at worst a loss in time value.

Each party in the swap will need to:
• create a contract script
• send a transaction to the contract
• create a transaction redeeming coins on the other chain

First, we are going to need some function to create the special contract script. This is going to be done similarly to how complex scripts were made in my hashLock and multisig script write-ups. The variable data in the contract script are: the two pubkey hashes, the hashlock and the lock time. These values are going to be passed to the contract function and it’s going to return a script object containing a HTLC.

script contractScript(payment_address seller, payment_address buyer, uint32_t locktime, short_hash hashLock)
{
	std::string scriptString = "if dup hash160 [" + encode_base16(buyer.hash()) + "] equalverify checksigverify hash160 [" + encode_base16(hashLock) + "] equal else [" + encode_base10(locktime) + "] checklocktimeverify drop dup hash160 [" + encode_base16(seller.hash()) + "] equalverify checksig";
	script contract = script();
	contract.from_string(scriptString);
	return contract;
}

For this contract factory, we can use the libbitcoin abstract data types for payment addresses but will need a way to set the locktime and hashlock.

The locktime can be represented as a block height or a Unix timestamp. For simplicity I’ve decided to use Unix time. This function will take an argument N, where n represents days locked, and multiply it by 86400(the number of seconds per day) and then add it to the current Unix time thus returning the uint32_t timestamp of N days in the future.

uint32_t setLocktime(uint32_t days)
{
	uint32_t seconds = days*86400;
	uint32_t now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
	uint32_t timeLock = now + seconds;
	return timeLock;
}

Now, recall that the contract script is checking the hashlock by hash160ing an argument. Our hashLock function is going to take a string, the password/preimage, and convert it to a data chunk before returning a bitcoin short hash(same as hash160, same as ripemd160(sha256(data))).

short_hash makeHashLock(std::string secret)
{
	data_chunk secretChunk = to_chunk(secret);
	short_hash hashLock = bitcoin_short_hash(secretChunk);
	return hashLock;
}

With those two helper functions, the contract factory should now work. We can test this from the main function.

int main(){
	payment_address aliceBTC_address("mpEwnuvRJxRC7R4KxjdooMFRUNRYaQxwWD");
	payment_address bobBTC_address("moRFyt8S8YTCJkgHktoqVZ73zWb8crevh2");
	script BTCcontract = contractScript(aliceBTC_address, bobBTC_address, setLocktime(14), makeHashLock("Libbitcoin"));
	payment_address BTCcontract_address = payment_address(BTCcontract, 196);

	std::cout << "BTC contract Address: " << BTCcontract_address.encoded() << std::endl;
	std::cout << "HashLock: " << encode_base16(makeHashLock("Libbitcoin")) << "\n" <<std::endl;
std::cout << "Contract Script: " << encode_base16(BTCcontract.to_data(0)) << std::endl;

This script can be inspected using BX:

alpha$ bx script-decode 

Now, our traders need a way to send the amount of coin they want to exchange to the contract address. This is achieved by constructing a standard p2sh transaction from our public key, to do so we need to pass our transaction-building function the payment address of the contract, the amount we want to send, and the private key to sign with. Now, inside our pay-to-contract function we can set the inputs from our address, outputs to the contract and sign it using another helper function to push the sig script into the transaction.

transaction payToContract(payment_address contract, uint64_t amount, ec_private key)
{
	transaction tx = transaction();
	hash_digest utxoHash; 
	decode_hash(utxoHash, "0e600c5a7ee3055dee38b08379a020fa5eb013bdffe8742fb9f0a802e7011d50");
	output_point utxo(utxoHash, 0);
	input input1 = input();
	input1.set_previous_output(utxo);
	input1.set_script(script(script().to_pay_key_hash_pattern(key.to_payment_address())));
	tx.inputs().push_back(input1);

	script outputScript(script().to_pay_script_hash_pattern(contract.hash()));
	output output1(amount, outputScript);
	tx.outputs().push_back(output1);

	return sigScript(key, tx);

}

transaction sigScript(ec_private wal, transaction tx)
{
	int index = 0;
	for (auto input: tx.inputs())
	{

		endorsement sig;
		script().create_endorsement(sig, wal.secret(), input.script(), tx, index, all);
		operation::list ops {operation(sig), operation(to_chunk(wal.to_public().point()))};
		script scriptSig(ops);
		input.script().clear();
		input.set_script(scriptSig);
		tx.inputs()[index] = input;
		index++;
	}
	return tx;

}

Again, we can test our transaction builder from the main function.

int main(){
	payment_address aliceBTC_address("mpEwnuvRJxRC7R4KxjdooMFRUNRYaQxwWD");
	payment_address bobBTC_address("moRFyt8S8YTCJkgHktoqVZ73zWb8crevh2");
	script BTCcontract = contractScript(aliceBTC_address, bobBTC_address, setLocktime(14), makeHashLock("Libbitcoin"));
	payment_address BTCcontract_address = payment_address(BTCcontract, 196);

	std::cout << "BTC contract Address: " << BTCcontract_address.encoded() << std::endl;
	std::cout << "HashLock: " << encode_base16(makeHashLock("Libbitcoin")) << "\n" <<std::endl;
std::cout << "Contract Script: " << encode_base16(BTCcontract.to_data(0)) << std::endl;
	transaction BTCpayment = payToContract(BTCcontract_address, 100000000, alicBTC_private);
	std::cout << encode_base16(BTCpayment.to_data(1)) << "\n" << std::endl;
	ec_private alicBTC_private("cR3FqqoLz6b5wmA5h7LHzTJYosbcuZGsrQ2Fse8V2q2jQtesarmg", 239);

Now, the first two major functionalities of the swap are implemented and we only require a method withdrawing money from the contract once the terms have been met but in order to do so we will need some participation from the other side of this exchange.

We can test the LTC side of this transaction in the main function, note that that the only difference here is that the address prefixes will are passed through manually and will be different depending on the chain used.

For this cross-testnet example; however, P2SH-address and private-key prefixes are the same for both coins.

int main(){
	payment_address aliceBTC_address("mpEwnuvRJxRC7R4KxjdooMFRUNRYaQxwWD");
	payment_address bobBTC_address("moRFyt8S8YTCJkgHktoqVZ73zWb8crevh2");
	script BTCcontract = contractScript(aliceBTC_address, bobBTC_address, setLocktime(14), makeHashLock("Libbitcoin"));
	payment_address BTCcontract_address = payment_address(BTCcontract, 196);

	std::cout << "BTC contract Address: " << BTCcontract_address.encoded() << std::endl;
	std::cout << "HashLock: " << encode_base16(makeHashLock("Libbitcoin")) << "\n" <<std::endl;
std::cout << "Contract Script: " << encode_base16(BTCcontract.to_data(0)) << std::endl;

	transaction BTCpayment = payToContract(BTCcontract_address, 100000000, alicBTC_private);
	std::cout << encode_base16(BTCpayment.to_data(1)) << "\n" << std::endl;
	ec_private alicBTC_private("cR3FqqoLz6b5wmA5h7LHzTJYosbcuZGsrQ2Fse8V2q2jQtesarmg", 239);

short_hash hashLock;
	decode_base16(hashLock, "7e88c8277e78610110c79a77eb0d340fba0c2775");
	script LTCcontract = contractScript(bobBTC_address, aliceBTC_address, setLocktime(7), hashLock);
	payment_address LTCcontract_address = payment_address(LTCcontract, 196);
	std::cout << "LTC contract Address: " << LTCcontract_address.encoded() << "\n"<<std::endl;
	transaction LTCpayment = payToContract(LTCcontract_address, 1000000000, bobLTC_private);
	std::cout << encode_base16(LTCpayment.to_data(1)) << "\n" <<std::endl;

Now that both parties have committed their coins to the exchange, Alice can claim her tLTC from the LTC contract. Once, to do this her sig script will have to include the secret pre-image, then Bob will be able to use it to create his withdraw transaction.

The withdraw transaction is going to sign the UTXO of the contract payment to the owner’s public key and will of course need to provide the Hash in order to do this. The script will need to start with the redeem script followed by opcode 81, which pushes the value 1 (aka TRUE) onto the stack telling the redeem script to execute the first if block.


transaction withdrawContract(transaction contractPayment, script redeemScript, uint64_t amount, data_chunk hashKey, ec_private key)
{
	transaction tx = transaction();
	// hash_digest utxoHash;
	// decode_hash(utxoHash, "");
	output_point utxo(contractPayment.hash(), 0);

	input input1 = input();
	input1.set_previous_output(utxo);
	tx.inputs().push_back(input1);

	script outputScript(script().to_pay_key_hash_pattern(key.to_payment_address().hash()));
	output output1(amount, outputScript);
	tx.outputs().push_back(output1);

	endorsement sig; 
	script().create_endorsement(sig, key.secret(), redeemScript, tx, 0, all);
	operation::list resolveHTLC {operation(hashKey), operation(sig), operation(to_chunk(key.to_public().point())), operation(opcode(81)), operation(redeemScript.to_data(0))};

	tx.inputs()[0].set_script(script(resolveHTLC));

	return tx;
}

Outputting Alice’s transaction in main():

int main(){
	payment_address aliceBTC_address("mpEwnuvRJxRC7R4KxjdooMFRUNRYaQxwWD");
	payment_address bobBTC_address("moRFyt8S8YTCJkgHktoqVZ73zWb8crevh2");
	script BTCcontract = contractScript(aliceBTC_address, bobBTC_address, setLocktime(14), 
         makeHashLock("Libbitcoin"));
	payment_address BTCcontract_address = payment_address(BTCcontract, 196);

	std::cout << "BTC contract Address: " << BTCcontract_address.encoded() << std::endl;
	std::cout << "HashLock: " << encode_base16(makeHashLock("Libbitcoin")) << "\n" <<std::endl;
        std::cout << "Contract Script: " << encode_base16(BTCcontract.to_data(0)) << std::endl;

	transaction BTCpayment = payToContract(BTCcontract_address, 100000000, alicBTC_private);
	std::cout << encode_base16(BTCpayment.to_data(1)) << "\n" << std::endl;
	ec_private alicBTC_private("cR3FqqoLz6b5wmA5h7LHzTJYosbcuZGsrQ2Fse8V2q2jQtesarmg", 239);
        short_hash hashLock;
	decode_base16(hashLock, "7e88c8277e78610110c79a77eb0d340fba0c2775");
	script LTCcontract = contractScript(bobBTC_address, aliceBTC_address, setLocktime(7), hashLock);
	payment_address LTCcontract_address = payment_address(LTCcontract, 196);
	std::cout << "LTC contract Address: " << LTCcontract_address.encoded() << "\n"<<std::endl;
	transaction LTCpayment = payToContract(LTCcontract_address, 1000000000, bobLTC_private);
	std::cout << encode_base16(LTCpayment.to_data(1)) << "\n" <<std::endl;


    std::cout << encode_base16(withdrawContract(LTCpayment, LTCcontract, 1000000000, to_chunk("Libbitcoin"), alicBTC_private).to_data(1)) << "\n" << std::endl;

    std::cout << encode_base16(withdrawContract(BTCpayment, BTCcontract, 100000000, to_chunk("Libbitcoin"), bobLTC_private).to_data(1)) << std::endl;

}

If for some reason Alice refused to communicate the password to Bob but still claimed the LTC funds, Bob can simply look on the blockchain and see the password in the unlocking script.

If Alice refuses to claim the LTC, then Bob just needs to wait until the locktime to claim his refund.

2 comments on “Libbitcoin: Cross-Chain Swaps With HTLCs

  1. Thank you for great article! It looks like you need to replace first OP_CHECKSIG with OP_CHECKSIGVERIFY

Leave a Reply

Your email address will not be published.