PurrSettle is a settlement mechanism for Bitcoin zero confirmation transactions. It is a derivative concept of this paper which makes use OP_AND
which is currently disabled. This mechanism makes use of the OP_CAT
opcode which is also disabled but allows for a more cost-effective version to ensure a penalty mechanism is in place when a malicious actor tries to double spend their coins. At the heart of this mechanism, is the commitment of a nonce within the script that will be used to create signatures. If a malicious actor tries to double spend their coins, they risk revealing their private key making it easy for onlookers aware of the protocol to sweep all their coins.
An example implementation can be found on GitHub.
PurrScript: How it works
The key behind this mechanism is the OP_CAT
opcode. This opcode takes two inputs and concatenates them together. As described before, this will be used to generate the signature while the script is being evaluated. One part of the signature (the nonce) is committed in advance into the script. The other part of signature is provided with the unlocking script (the witness). If you’re aware of the Bitcoin ecosystem, there are two types of signatures available:
- Schnorr Signatures
- ECDSA Signatures
PurrSettle makes use of Schnorr signatures. The primary reason is because ECDSA signatures are DER
encoded and as a result, the signatures have variable length encoding within them. It is not possible to know the length of the signature beforehand. This makes it difficult to create the signature during script evaluation. A plausible way to work with ECDSA signatures is to provide the length and remaining signature to the unlocking script and shuffling around the stack items to get the signature. Schnorr signatures are easier to work with as they’re just stored as [R, s]
pairs.
With the nonce committed in the script, and the signature provided with the witness, the complete signature can be generated by concatenating both of them. Note that you’ll have to swap the order of the items in the stack before generating the signature. You can look at the script creation in our implementation here. Here is how the script evaluation looks like given that the correct signature and pubkey combination has been passed:
Stack | Script |
---|---|
s | OP_PUSHBYTES_32 R OP_SWAP OP_CAT OP_PUSHBYTES_32 P OP_CHECKSIG |
s R | OP_SWAP OP_CAT OP_PUSHBYTES_32 P OP_CHECKSIG |
R s | OP_CAT OP_PUSHBYTES_32 P OP_CHECKSIG |
Rs | OP_PUSHBYTES_32 P OP_CHECKSIG |
Rs P | OP_CHECKSIG |
1 |
Extracting private key from double spending transactions
In the event of a double-spending attempt, the attacker is compelled to reuse the same nonce in the signature process. This creates a critical vulnerability, making it straightforward to extract the private key of the individual attempting the double spend. The process of deriving the private key under these circumstances can be illustrated through the following Schnorr signature equations:
Where
Subtracting (2) from (1) gives us:
These signatures follow the BIP-0340 scheme for signing. The extracted private key can be used to sweep all of the perpetrator’s coins. This includes P2PK, P2WPKH, P2SH-P2WPKH, P2TR (key-path spends) UTXOs.
Note that all information required to extract the private key is available in the transaction. An example that extracts the private keys from these transactions can be found here. Instructions for running that test case can be found in the readme.
Making PurrSettle transactions the norm
By now, you may have realised that you simply cannot use the same UTXO twice without being penalised. This means that change outputs should be sent to new PurrScripts and not the PurrScript you’re spending from since you will reveal your private key in the process. Using PurrSettle requires you to store the nonces. Or, you may create deterministic schemes to create the nonce.
One idea is to use BIP32 HD paths to create nonces instead of private key. For reference, the old key derivation path was as follows: m'/xx'/coin_type'/account'/key_chain/index
. The new path would look like: m'/xx'/coin_type'/account'/key_or_nonce/key_chain/index
where another node is inserted after the account to indicate whether you’re deriving a private key or a nonce. While it is not backwards-compatible with the previous key generation scheme, it allows us to easily keep track of nonces while also inflicting possibly greater penalty on double spending. If the private key (xpriv) is ever revealed during this scheme, and if the xpub of m/xx'/coin'/account'
is known, then all nonces and all private keys that have been used so far can be derived and sweeped.
Closing thoughts
PurrSettle is not without its challenges. The requirement to manage nonces carefully and the risks associated with incorrect usage—such as inadvertently revealing private keys—mean that users must exercise caution when implementing PurrSettle. Additionally, the reliance on a disabled opcode like OP_CAT means that this technique is currently limited to experimental environments like StarkWare’s public signet. While PurrSettle may not yet be ready for mainstream adoption, it opens the door to new possibilities in Bitcoin’s scripting capabilities.