Sitemap

Bitcoin Taproot Puzzles

5 min readMay 20, 2025

--

In 2013, Peter Todd published a few transactions with puzzles in them. The trick was to find hashing algorithm collisions. People who found the collision were able to spend the bitcoin locked with those hashes.

When I saw this, I was like… I can’t even make a single transaction from scratch programmatically.

This guide will help you create your very own puzzles or participate in any hackathon with these puzzles.

WARNING: spending UTXOs without a signature is highly risky, and attackers will most likely steal your funds if using the Bitcoin main net.
For this guide, I’m using testnet4.

For simplicity, I use a very simple Script and not a complicated one like Todd’s. Here is the ASM script I will be using for this tutorial:

OP_SHA1 <HASH> OP_EQUAL

First, if you haven’t already learn how to setup your environment, how to create your wallet addresses, how to fund your address, or how to publish transactions, head over to my Bitcoin Taproot Hello World turorial:

Fund your taproot address

Create and fund a new taproot address and store the funding transaction id because you will need it to spend the Bitcoin you received from the faucet. For this tutorial, I use the following:

Mnemonic: jaguar debate issue visit diamond recipe pyramid bulb diet electric immense shaft
Taproot Address: tb1peextn6a2n9d2c0s0u5jwrflr4fusmvnlnm70hchs4j006ml3m56qdqseu8
Funding Transaction: 1b186fd9af04f0c71e7e3c5ad0a6fe6265f258ee78bc6ade9918fe6ff57e330f

Create SHA1 hack-locked script

For this tutorial, I’m going to use hello puzzle as the secret:

const preimage = Buffer.from('hello puzzle');
const sha1Hash = crypto.createHash('sha1').update(preimage).digest();
const sha1Script = bitcoin.script.compile([
bitcoin.opcodes.OP_SHA1,
sha1Hash,
bitcoin.opcodes.OP_EQUAL,
]);

Now, when creating the taproot output p2tr address, you will include this script as follows:

// Taproot leaf version (currently the only supported version)
const leafVersion = 0xc0;

// Create Taproot script tree
const scriptTree = {
output: sha1Script,
version: leafVersion, // Default leaf version for TapScript
};

// Create Taproot output with script tree
const taprootOutput = bitcoin.payments.p2tr({
internalPubkey,
scriptTree,
network,
});

Now use the created p2tr address in out of the outputs, and I’m going to use 1,000 sats as the reward for whoever unlocks the puzzle

// Add Taproot output (lock funds with SHA1 script)
const lockAmount = 1000n;
psbt.addOutput({
address: taprootOutput.address,
value: lockAmount,
});

The rest of the code in Hello World is the same.

When I run my code, I get the following transaction hex

020000000001010f337ef56ffe1899de6abc78ee58f26562fea6d05a3c7e1ec7f004afd96f181b0100000000fdffffff02e803000000000000225120042e9b5eabfa28eae55793a3bbc740ccb8173220584d3fb8bc88acdcbfe2d827000f000000000000225120c64937cf3b01d1558f87a89a4bd8f60c764f7823e3916899663fdf986021466f014047c72e2019eeb85d1549632ff00519427ed6d3b8067163a963bd94eda2ca763ed0e135ae5254cbcd04bb1e04a7ec3fe86707702c849d8add9e2c00e84c1eb5eb00000000

I test it:

Press enter or click to view image in full size

and then broadcast it:

Press enter or click to view image in full size
Press enter or click to view image in full size

As you can see, one of the outputs has 1,000 sats, that’s where we will need out puzzle to unlock the fund. Copy the transaction ID, you’re going to need it to spend the UTXO with the puzzle

Unlock funds with the puzzle

This is where the players try, and try, and try to find the preimage with the hash you give them. In this case, the SHA1 hash for our “secret” preimage is:

098f9ed44ff560c7d8529068fde96911b6262ff9

For someone to spend the locked sats, they need:

  1. Locking Transaction ID, in our case:
TxID: 0934fc3c17f42e1c77265cb0345ab3ac3f9e3d40c670c62f0ba0aaa6f8bf8e9c

2. ScriptPubKey (HEX) can be found on an explorer:

Press enter or click to view image in full size
ScriptPubKey: 5120042e9b5eabfa28eae55793a3bbc740ccb8173220584d3fb8bc88acdcbfe2d827

3. The Unlocking ASM Script to recreate the script, or hex, for simplicity, let’s use the hex:

a714098f9ed44ff560c7d8529068fde96911b6262ff987

4. The Control Block: (hex):

c020a1a7dc240dd0de656aa7aa4b6b758c22de21db7b91a2570b014cbc4d72c9d4

Let’s put it all together:

const p2tr_output_buffer = Buffer.from('5120042e9b5eabfa28eae55793a3bbc740ccb8173220584d3fb8bc88acdcbfe2d827', 'hex');
const sha1Script_buffer = Buffer.from('a714098f9ed44ff560c7d8529068fde96911b6262ff987', 'hex');
const controlBlock_buffer = Buffer.from('c020a1a7dc240dd0de656aa7aa4b6b758c22de21db7b91a2570b014cbc4d72c9d4', 'hex');

// Create PSBT
const psbt = new bitcoin.Psbt({ network });
psbt.addInput({
hash: lockingTxId,
index: lockingIndex,
witnessUtxo: {
script: p2tr_output_buffer,
value: lockedAmount,
},
tapLeafScript: [
{
leafVersion: leafVersion,
script: sha1Script_buffer,
controlBlock: controlBlock_buffer,
},
],
});


psbt.addOutput({
address: dest.address,
value: lockedAmount - fee,
});

Finalize the psbt transaction:

// Provide witness stack
psbt.finalizeInput(0, () => ({
finalScriptSig: undefined,
finalScriptWitness: witnessStackToScriptWitness([
preimage,
sha1Script_buffer,
controlBlock_buffer,
]),
}));

// Final TX
const tx = psbt.extractTransaction();

In the above script, preimage if the preimage the player is iterating, the solution is:

const preimage = Buffer.from('hello puzzle');

After running the code, I get transaction hex:

020000000001019c8ebff8a6aaa00b2fc670c6403d9e3facb35a34b05c26771c2ef4173cfc34090000000000ffffffff014803000000000000225120ce4cb9ebaa995aac3e0fe524e1a7e3aa790db27f9efcfbe2f0ac9efd6ff1dd34030c68656c6c6f2070757a7a6c6517a714098f9ed44ff560c7d8529068fde96911b6262ff98721c020a1a7dc240dd0de656aa7aa4b6b758c22de21db7b91a2570b014cbc4d72c9d400000000
Press enter or click to view image in full size

And I broadcast the transaction:

Press enter or click to view image in full size

Below you can see the broadcasted transaction, and also see a red unlock icon, meaning that it is not signed, and risk losing our funds, but because this is testnet4, nobody is trying to steal fake money:

Press enter or click to view image in full size

Spending funds via Key Path

There is another way to spend the funds, Taproot allows spending UTXO in multiple ways.

Remember that when creating the p2tr we also include our public key, well, this allows us to spend the funds without solving the puzzle, but just by signing the input as follows:

const taprootKeyPair = ECPair.fromPrivateKey(child.privateKey, { network: network });
// Internal public key (x-only, 32 bytes)
const internalPubkey2 = taprootKeyPair.publicKey.slice(1, 33);

// Create p2tr payment with full tapleaf script tree and redeem
const p2tr2 = bitcoin.payments.p2tr({
internalPubkey: internalPubkey2,
scriptTree: {
output: sha1Script,
version: leafVersion
},
network,
});

const psbt2 = new bitcoin.Psbt({ network });
psbt2.addInput({
hash: lockingTxId,
index: lockingIndex,
witnessUtxo: {
script: p2tr2.output,
value: lockedAmount,
},
tapInternalKey: internalPubkey2,
sequence: 0xfffffffd, // Enable RBF
});

psbt2.addOutput({
address: dest.address,
value: lockedAmount - fee,
});

// Sign the input
const tweakedChildNode = taprootKeyPair.tweak(
Buffer.from(
bitcoin.crypto.taggedHash(
'TapTweak',
Buffer.concat([
internalPubkey2,
bitcoin.crypto.taggedHash('TapLeaf', Buffer.concat([
Buffer.from([leafVersion]), // Leaf version
Buffer.from([sha1Script.length]), // Script length
sha1Script,
])),
])
)
)
);

psbt2.signInput(0, tweakedChildNode);

// Provide witness stack
psbt2.finalizeAllInputs();

// Final TX
const tx2 = psbt2.extractTransaction();
console.log('Keyspend - Transaction Hex:', tx2.toHex());

This is useful if you want to cancel the game.

Conclusion

You learned how to create a UTXO spendable with a puzzle (script) or Key Path, and also learned how to spend it.

Source Code: https://github.com/ObjSal/btc-puzzle

Much more complicated than the Hello World tutorial. Feel free to write a question below, and I will try my best to answer it.

Good Luck!

--

--

Salvador Guerrero
Salvador Guerrero

Written by Salvador Guerrero

Computer Science Engineer, Cross-Platform App Developer, Open Source contributor. 🇲🇽🇺🇸