Skip to content

Instantly share code, notes, and snippets.

@chris-belcher
Last active November 14, 2022 10:38
Show Gist options
  • Save chris-belcher/7257763cedcc014de2cd4239857cd36e to your computer and use it in GitHub Desktop.
Save chris-belcher/7257763cedcc014de2cd4239857cd36e to your computer and use it in GitHub Desktop.
Timelocked addresses fidelity bond BIP
  BIP: TBD. Preferably a two-digit number to match the bip44, bip49, bip84, bip86 family of bips
  Layer: Applications
  Title: Derivation scheme for storing timelocked address fidelity bonds in BIP39 phrases
  Author: Chris Belcher 
  Status: Draft
  Type: Standards Track
  Comments-Summary: No comments yet.
  Created: 2022-04-01
  License: CC0-1.0

Abstract

This BIP defines the derivation scheme for BIP39 seed phrases which create timelocked addresses used for creating fidelity bonds. It also defines how to sign fidelity bond certificates, which are needed when using fidelity bonds that are stored offline.

Motivation

Fidelity bonds are used to resist sybil attacks in certain decentralized anonymous protocols. They are created by locking up bitcoins using the OP_CHECKLOCKTIMEVERIFY opcode.

It would be useful to have a common derivation scheme so that users of wallet software can have a backup of their fidelity bonds by storing only the BIP39 seed phrase and a reference to this BIP. Importantly the user does not need to backup any timelock values.

We largely use the same approach used in BIPs 49, 84 and 86 for ease of implementation.

This standard is already implemented and deployed in JoinMarket. As most changes would requires a protocol change of a live system, there is limited scope for changing this standard in review. This BIP is more about documenting something which already exists, warts and all.

It would be useful to be able to keep the private keys of fidelity bonds in cold storage. This would allow the sybil resistance of a system to increase without hot wallet risk.

Background

Fidelity bonds

A fidelity bond is a mechanism where bitcoin value is deliberately sacrificed to make a cryptographic identity expensive to obtain. A way to create a fidelity bond is to lock up bitcoins by sending them to a timelocked address. The valuable thing being sacrificed is the time-value-of-money.

The sacrifice must be done in a way that can be proven to a third party. This proof can be made by showing the UTXO outpoint, the address redeemscript and a signature which signs a message using the private key corresponding to the public key in the redeemscript.

The sacrificed value is an objective measurement that can't be faked and which can be verified by anybody (just like, for example PoW mining). Sybil attacks can be made very expensive by forcing a hypothetical sybil attacker to lock up many bitcoins for a long time. JoinMarket implements fidelity bonds for protection from sybil attackers. At the time of writing over 600 BTC in total have been locked up with some for many years. Their UTXOs and signatures have been advertised to the world as proof. We can calculate that for a sybil attacker to succeed in unmixing all the CoinJoins, they would have to lock up over 100k BTC for several years.

Fidelity bonds in cold storage

To allow for holding fidelity bonds in cold storage, there is an intermediate keypair called the certificate.

UTXO key ---signs---> certificate ---signs---> endpoint

Where the endpoint might be a IRC nickname or Tor onion hostname. The certificate keypair can be kept online and used to prove ownership of the fidelity bond. Even if the hot wallet private keys are stolen, the coins in the timelocked address will still be safe, although the thief will be able to impersonate the fidelity bond until the expiry.

Fixed timelock values

It would be useful for the user to avoid having to keep a record of the timelocks in the time-locked addresses. So only a limited small set of timelocks are defined by this BIP. This way the user must only store their seed phrase, and knowledge that they have coins stored using this BIP standard. The user doesn't need to remember or store any dates.

Specifications

This BIP defines the two needed steps to derive multiple deterministic addresses based on a [[bip-0032.mediawiki|BIP 32]] master private key. It also defines the format of the certificate can be signed by the deterministic address key.

Public key derivation

To derive a public key from the root account, this BIP uses a similar account-structure as defined in BIP [[bip-0084.mediawiki|44]] but with change set to 2.

m / 84' / 0' / 0' / 2 / index

A key derived with this derivation path pattern will be referred to as derived_key further in this document.

For index, addresses are numbered from 0 in a sequentially increasing manner, but index does not increase forever like in other similar standards. The index only goes up to 959 inclusive. Only 960 addresses can be derived for a given BIP32 master key. Furthermore there is no concept of a gap limit, instead wallets must always generate all 960 addresses and check all of them if they have a balance and history.

Timelock derivation

The timelock used in the time-locked address is derived from the index. The timelock is a unix time. It is always the first of the month at midnight. The index counts upwards the months from January 2020, ending in December 2099. At 12 months per year for 80 years this totals 960 timelocks. Note that care must be taken with the year 2038 problem on 32-bit systems.

year = 2020 + index // 12
month = 1 + index % 12

Address derivation

To derive the address from the above calculated public key and timelock, we create a redeemScript which locks the funds until the timelock, and then checks the signature of the derived_key. The redeemScript is hashed with SHA256 to produce a 32-byte hash value that forms the scriptPubKey of the P2WSH address.

redeemScript: <timelock> OP_CHECKLOCKTIMEVERIFY OP_DROP <derived_key> OP_CHECKSIG
witness:      <signature> <pubkey>
scriptSig:    (empty)
scriptPubKey: 0 <32-byte-hash>
              (0x0020{32-byte-hash})

Message signing

In order to support signing of certificates, implementors should support signing ascii messages.

A certificate message can be created by another application external to this standard. It is then prepended with the string \x18Bitcoin Signed Message:\n and a byte denoting the length of the certificate message. The whole thing is then signed with the private key of the derived_key. This part is identical to the "Sign Message" function which many wallets already implement.

Almost all wallets implementing this standard can use their already-existing "Sign Message" function to sign the certificate message. As the certificate message itself is always an ascii string, the wallet may not need to specially implement this section at all but just rely on users copypasting their certificate message into the already-existing "Sign Message" user interface. This works as long as the wallet knows how to use the private key of the timelocked address for signing messages.

It is most important for wallet implementions of this standard to support creating the certificate signature. Verifying the certificate signature is less important.

Test vectors

mnemonic = abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
rootpriv = xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu
rootpub  = xpub661MyMwAqRbcFkPHucMnrGNzDwb6teAX1RbKQmqtEF8kK3Z7LZ59qafCjB9eCRLiTVG3uxBxgKvRgbubRhqSKXnGGb1aoaqLrpMBDrVxga8

// First timelocked address = m/84'/0'/0'/2/0
derived private_key = L2tQBEdhC48YLeEWNg3e4msk94iKfyVa9hdfzRwUERabZ53TfH3d
derived public_key  = 02a1b09f93073c63f205086440898141c0c3c6d24f69a18db608224bcf143fa011
unix locktime       = 1577836800
string locktime     = 2020-01-01 00:00:00
redeemscript        = 0400e10b5eb1752102a1b09f93073c63f205086440898141c0c3c6d24f69a18db608224bcf143fa011ac
scriptPubKey        = 0020bdee9515359fc9df912318523b4cd22f1c0b5410232dc943be73f9f4f07e39ad
address             = bc1qhhhf29f4nlyalyfrrpfrknxj9uwqk4qsyvkujsa7w0ulfur78xkspsqn84

// Test certificate using first timelocked address
// Note that as signatures contains a random nonce, it might not be exactly the same when your code generates it
// p2pkh address is the p2pkh address corresponding to the derived public key, it can be used to verify the message
//  signature in any wallet that supports Verify Message.
// As mentioned before, it is more important for implementors of this standard to support signing such messages, not verifying them
Message       = fidelity-bond-cert|020000000000000000000000000000000000000000000000000000000000000001|375
Address       = bc1qhhhf29f4nlyalyfrrpfrknxj9uwqk4qsyvkujsa7w0ulfur78xkspsqn84
p2pkh address = 16vmiGpY1rEaYnpGgtG7FZgr2uFCpeDgV6
Signature     = H2b/90XcKnIU/D1nSCPhk8OcxrHebMCr4Ok2d2yDnbKDTSThNsNKA64CT4v2kt+xA1JmGRG/dMnUUH1kKqCVSHo=

// 2nd timelocked address = m/84'/0'/0'/2/1
derived private_key = KxctaFBzetyc9KXeUr6jxESCZiCEXRuwnQMw7h7hroP6MqnWN6Pf
derived public_key  = 02599f6db8b33265a44200fef0be79c927398ed0b46c6a82fa6ddaa5be2714002d
unix locktime       = 1580515200
string locktime     = 2020-02-01 00:00:00
redeemscript        = 0480bf345eb1752102599f6db8b33265a44200fef0be79c927398ed0b46c6a82fa6ddaa5be2714002dac
scriptPubKey        = 0020b8f898643991608524ed04e0c6779f632a57f1ffa3a3a306cd81432c5533e9ae
address             = bc1qhrufsepej9sg2f8dqnsvvaulvv490u0l5w36xpkds9pjc4fnaxhq7pcm4h

// timelocked address after the year 2038 = m/84'/0'/0'/2/240
derived private_key = L3SYqae23ZoDDcyEA8rRBK83h1MDqxaDG57imMc9FUx1J8o9anQe
derived public_key  = 03ec8067418537bbb52d5d3e64e2868e67635c33cfeadeb9a46199f89ebfaab226
unix locktime       = 2208988800
string locktime     = 2040-01-01 00:00:00
redeemscript        = 05807eaa8300b1752103ec8067418537bbb52d5d3e64e2868e67635c33cfeadeb9a46199f89ebfaab226ac
scriptPubKey        = 0020e7de0ad2720ae1d6cc9b6ad91af57eb74646762cf594c91c18f6d5e7a873635a
address             = bc1qul0q45njptsadnymdtv34at7karyva3v7k2vj8qc7m2702rnvddq0z20u5

// last timelocked address = m/84'/0'/0'/2/959
derived private_key = L5Z9DDMnj5RZMyyPiQLCvN48Xt7GGmev6cjvJXD8uz5EqiY8trNJ
derived public_key  = 0308c5751121b1ae5c973cdc7071312f6fc10ab864262f0cbd8134f056166e50f3
unix locktime       = 4099766400
string locktime     = 2099-12-01 00:00:00
redeemscript        = 0580785df400b175210308c5751121b1ae5c973cdc7071312f6fc10ab864262f0cbd8134f056166e50f3ac
scriptPubKey        = 0020803268e042008737cf439748cbb5a4449e311da9aa64ae3ac56d84d059654f85
address             = bc1qsqex3czzqzrn0n6rjayvhddygj0rz8df4fj2uwk9dkzdqkt9f7zs5c493u

Code generating these test vectors can be found here: https://github.com/chris-belcher/timelocked-addresses-fidelity-bond-bip-testvectors

Reference

@bigspider
Copy link

Playing with the miniscript compiler, it appears to me that the script is equivalent to:

<derived_key> OP_CHECKSIGVERIFY <timelock> OP_CHECKLOCKTIMEVERIFY

which avoids the OP_DROP, and comes from compiling the policy and(pk(derived_key),after(timelock)).

@chris-belcher
Copy link
Author

@bigspider Wow great find!

Unfortunately that form of the script is already deployed in JoinMarket, and changing it would make the existing timelocked addresses not be recoverable with the bip39 seed phrases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment