Blockchain

Bitcoin’s Time Locks

Bitcoin, having no discernible faults, comes equipped with several different time locks. These tools allow you to specify time-based conditions under which transactions are valid. Using time locks you make a transaction now that pays someone next week, add a mandatory waiting period for coin movements, set up complex smart contracts that flow across several transactions, or accidentally lock up your coins for centuries.

Most of these time locks were added to Bitcoin quite recently. They’re built into the structure of transactions, and have more than a few idiosyncrasies left over from buggy code written by our favorite anonymous cypherpunk, Mr. Nakamoto. The corresponding Bitcoin Improvement Proposals (BIPs) are wonderful and detailed, but assume a lot of background knowledge. This is an attempt to compile all the information I can find on the time locks, and explain it in depth.

Classifying Time Locks

Before we dive into the tools themselves, let’s figure out how to describe their operation. Time locks have three important attributes: location, targeting, and metric.

Location: Transaction vs. Script

Time is the longest distance between two places.
— Tennessee Williams

Time locks can be found in the transaction itself and/or in its Pay to Script Hash (P2SH) inputs’ associated scripts. Every transaction has multiple time lock fields (they’re present even when not used). Scripts, on the other hand, can have zero or many time locks. In terms of functionality, transaction-level and script-level locks are superficially similar, but perform very different roles. Transaction-level time locks cause a transaction to be invalid until a certain time, regardless of the validity of the signatures and scripts. Script-level time locks will cause script evaluation to fail unless the transaction is also locked. A failed script evaluation makes the transaction invalid. In a nutshell, transaction-level locks determine when a transaction may be confirmed, while script-level locks determine whether a given scriptsig is valid.

The major difference between them is what exactly they lock. A transaction-level lock constrains only a specific transaction. Think of transaction-level locks as future-dating a check: I can write you a check that becomes valid later, but the date applies only to that check, and I could spend the money other ways you don’t know about. Script-level locks sets conditions on all transactions spending an output. In other words, Transaction-level locks affect what you can do with a transaction after it’s constructed, but Script-level locks determine what transactions can be made in the first place.

Transaction locks aren’t as useful as you might think. They don’t control coins, only spends. This is why all the fun stuff required OP_CLTV and OP_CSV. Using script-level locks and conditional logic (OP_IF) we can make complex scripts that can, for example allow multisig spends any time, or single signature spends after a certain amount of time has passed. This provides a lot of versatility to P2SH transactions.

Script-level time locks require that a corresponding transaction-level time lock is also included. Script-level locks rely on transaction-level locks for enforcement. Rather than checking the time from within the script, script-level locks check the transaction’s lock. This is elegant and economical, if a bit un-intuitive. The script checks that the transaction is locked at least as long as the script path. It treats the transaction lock as a guarantee that time has passed.

Targeting: Absolute vs. Relative

Time is an illusion, lunchtime doubly so.
— Douglas Adams

Well, really, they’re both relative. You get to choose arbitrary-origin-point-relative or previous-output-relative. But that’s the kind of meaningless pedantry I love.

When we time lock coins, we set a target for their release. Absolute locks define this target in terms of a set time. They pick an exact moment when the lock expires. Relative time locks define it as an amount of time elapsed since the previous output confirmed. It’s the difference between “meet me at 15:00” and “meet me in 4 hours.”

Transactions that are locked to an absolute time are invalid until that time has passed. This means that I can make a transaction years in advance, sign it, share it, and even post it publicly with a guarantee that it won’t be confirmed before its lock expires. I might use an absolute timestamp to send money to my children or create a savings account that you can deposit to, but can’t withdraw from until next year.

Relative locks, on the other hand, mark a transaction invalid until a certain amount of time has passed since the transaction’s previous outputs were confirmed. This is a subtle and powerful feature. The real beauty of relative lock-times is setting locks relative to un-broadcast, un-confirmed transactions. Once a transaction is confirmed, we can always set an absolute lock-time in its future. But to do that, you have to wait for it to confirm, and learn its confirmation time. Or set up a long lock in advance, which becomes an expiration time for your entire smart contract. Relative locks can be set on un-confirmed transactions, meaning that you can create and sign an entire multi-step smart contract in advance, and be guaranteed that its transactions will confirm in the order you expect, no matter when you execute it.

Metric: Blocks vs. Seconds

Then’s the time to time the time flies –
Like time flies like an arrow.
— Edison B. Schroeder

In Bitcoin, time is a consensual hallucination and lunchtime never comes. Transactions can’t exactly look at a clock on the wall, so we need to decide what “time” is. Bitcoin has two ways of measuring “time”: block number, and block timestamp. These were implemented as modes of operation for each time lock, instead of full separate lock mechanisms. You can specify a number of blocks for the lock, or a number of seconds. Both of these have their own complications. In practice, both of these metrics are accurate enough for real-world uses. But it’s important to understand their idiosyncrasies.

We often say that blocks follow a poisson distribution: they’re expected to come every 10 minutes. But this isn’t quite right. When hashpower is increasing, blocks come faster than expected. Hashpower goes offline or is pointed at other chains, blocks come slower. Difficulty adjusts every 2016 blocks (about every 2 weeks) to target 10 minutes, but blocks can slip a significant amount from where you’d expect them to be due to network conditions, or just random chance.

Timestamps are just as finicky. You see, in Bitcoin, time doesn’t always go forward. Due to consensus rules for block timestamps, time can sometimes reverse itself for a block or two. Or just stop for a minute. There are reasons for this, I promise. It pretty much always stays within a couple hours of “real” time. To make timestamp-based locks reliable in spite of this, they measure using ‘median time past’ (MTP) method described in BIP 113. Rather than using the current block’s timestamp, timestamp-based locks use the median timestamp of the previous 11 blocks. This smooths out time’s advance, and ensures that time never goes backwards.

 

 

The Locks

Now that we understand what we’re talking about, let’s talk about the tools themselves. There are four time lock options right now: nLocktime, nSequence, OP_CHECKLOCKTIMEVERIFY (OP_CLTV), and OP_CHECKSEQUENCEVERIFY (OP_CSV). Two of them are script-level, two are transaction-level.

nLocktime

nLocktime is the transaction-level, absolute time lock. It’s also the only time lock that was part of Satoshi’s Original Vision (SOV).

A transaction is a simple datastructure that contains fields for version, inputs, outputs, and a few other things. nLocktime has its own special field lock_time. It specifies a block number or time stamp. The transaction is not valid until that time has passed. Transactions made by Bitcoin core have the lock_time field set to the current block by default to prevent fee sniping. Times are expressed as an unsigned 32 bit integer. If time_lock is 0, it’s ignored. If it is 500,000,000 or above, it’s treated as a unix timestamp. So nLocktime can lock transactions for a 9500 years using block numbers, or until 2106ish using timestamps.

Curiously, the lock_time field is ignored entirely if all inputs have a sequence number of 0xFFFFFFFF (the max for a 32 bit integer). Opt-in Replace-By-Fee (RBF) signals similarly as described in BIP 125. Using sequence_no to signal is an artifact from Satoshi’s half-baked time lock implementation. And at this point we’d have to hard fork to change that. nLocktime and input sequence numbers were originally supposed to create a simple transaction update mechanism. The idea was that you could create a transaction with a lock-time, and then replace it by sending a new version with at least one higher sequence number.

Miners were supposed to drop transactions with lower sequence numbers from the mempool. If all inputs had the maximum sequence number, it meant there could be no more updates, and the transaction could clear regardless of the time lock. This was never fully implemented, and later abandoned. In a curios slip up, Satoshi seems to have assumed good behavior, which is not a reasonable assumption in Bitcoin. It’s impossible to guarantee that miners will see updated transactions, or drop older version if they do see newer ones. Miners will mine the most profitable version of a transaction, not the latest.

nLocktime examples:

# Most of the transaction is omitted. Using decimal for human readability.
# Using hex for sequence numbers due to the presence of flags.
# Transaction is invalid until block 499999999 (this is a Bad Idea)
tx_1:
  lock_time: 49999999
# Transaction is invalid until the MTP is 1514764800 (1/1/2018 0:00:00 GMT)
tx_2:
  lock_time: 1514764800
# No lock time. Transaction is valid immediately.
tx_3:
  lock_time: 0
# nLocktime lock is not in effect, because all sequence numbers are set to 0xFFFFFFFF
tx_4:
  lock_time: 3928420
  input_1:
    sequence_no: 0xFFFFFFFFnSequence

nSequence is the transaction-level relative time lock (technically, nSequence is actually input-level, more later). It repurposes the old sequence_no field of each input to invalidate transactions based on the time elapsed since the previous outputs’ confirmations. nSequence locks were introduced in BIP 68 and activated by soft fork in mid-2016. Satoshi gave us lemons, and we made nSequence time locks.

Sequence numbers have been around since the beginning. But because transaction replacement was never implemented (and wouldn’t have worked in the long run anyway), they became cruft. For years, the only thing they could do was disable nLocktime. Now, sequence numbers are used to enforce relative time locks on the the transaction level as described in BIP 68. nSequence locks are set on each input, and measured against the output that each input is consuming. This means that several different time lock conditions can be specified on the same transaction. In order for a transaction to be valid, all conditions must be satisfied. If even a single sequence lock is not met, the entire transaction will be rejected.

Bitcoin developers are amazing at upcycling, but sometimes you end up with a few knots in the wood. Because nSequence time locks re-purpose the existing sequence_no field, it has a few idiosyncrasies. The sequence field is 32 bits, but we can’t use all of them, as it would interfere with nLocktime and RBF signaling. In addition, sequence_no is one of the few places where we have leeway to make future changes. To balance these demands, nSequence was built to use only 18 of the 32 bits. This conserves 14 bits for any future uses we can come up with.

Two bits are flags that tell the node how to interpret the sequence_no field. The most significant bit is the disable flag. If the disable flag is set, nSequence locks are disabled. If the disable flag is not set, the rest of the sequence_no field is interpreted as a relative lock-time. Bit 22 (the 23rd least significant bit), is the type flag. If the type flag is set, the lock is specified in seconds. If the type flag is not set, the lock is specified in blocks.

The least significant 16 bits of the sequence_no are used to encode the target time. Unlike nLocktime, nSequence uses only 16 bits to encode the lock-time. This means nSequence time locks are limited to 65535 units. This allows for locks up to about 455 days when using blocks, but would only allow about 18 hours in seconds. To mitigate this, nSequence does not measure in seconds. Instead it uses chunks of 512 seconds. If the type flag is set, and the lock-time is set to 16 units, the input will be locked until 16 * 512 seconds have elapsed.

Transactions made by Bitcoin Core, by default, have the sequence_no of each input set to 0xFFFFFFFE. This enables nLocktime to discourage fee sniping as described above, and disables Replace-By-Fee. Replace-By-Fee transactions typically have the sequence_no of each input set to 0xFFFFFFFD. It’s worth noting at this point that RBF is not a protocol change, only a change in default mining policy. However, because nSequence locks require that the sequence_no field be set lower than 0xFFFFFFFD to be meaningful, all nSequence locked transactions are opting into RBF.

nSequence examples:

# Most of the transaction is omitted. Using decimal for human readability.
# Using hex for sequence numbers due to the presence of flags.
# This transaction is locked for 4096 second. Just over 1 hour.
tx_1:
  input_1:
    sequence_no: 0x00400008
    # Disable flag is not set, type flag is set. Input locked for 8 * 512 seconds.
# This transaction is not nSequence locked, but may be nLocktime locked, and allows RBF.
tx_2:
  input_1:
    sequence_no: 0xFEDC3210
    # Disable flag is set. nSequence locking disabled.
# This transaction is invalid until 16 blocks have elapsed since input_1's prevout confirms.
tx_3:
  input_1:
    sequence_no: 0x00000010  
    # Disable flag is not set, type flag not set. This input locked for 16 blocks.
  input_2:
    sequence_no: 0xFFFFFFFF  
    # Disable flag is set.
# This transaction is not time locked, but has opted to allow Replace-By-Fee.
tx_4:
  lock_time: 0
  input_1:
    sequence_no: 0xFFFFFFFE  
    # nSequence is disabled, nLocktime is enabled, RBF is not signaled.
  input_2:
    sequence_no: 0xFFFFFFFD  
    # nSequence is disabled, nLocktime is enabled, RBF is signaled.
# This transaction is not valid until block 506221
# It is also not valid until 87040 seconds have passed since the confirmation of input_1's previous output
tx_5:
  lock_time: 506221
  input_1:
    sequence_no: 0x004000AA

 

 

CLTV

OP_CHECKLOCKTIMEVERIFY (OP_CLTV) is the script-level absolute time lock. It was detailed in BIP 65 and softforked into mainnet in late 2015. OP_CLTV enabled hashed timelocked contracts and as such was a hard requirement for the first version of Lightning channels.

Its source is simple and elegant, comprising less than 20 lines of clean, superbly-commented code. Put simply: OP_CLTV compares the top item of the stack to the transaction’s nLocktime. It checks that the top item of the stack is a valid time in seconds or blocks, and that the transaction itself is locked for at least that long via an appropriate lock_time. In this way, OP_CLTV checks that the transacion can’t be confirmed before a certain time.

OP_CHECKLOCKTIMEVERIFY causes script evaluation to fail immediately in the following five situations:

  1. The stack is empty (i.e. there’s no target time specified for OP_CLTV to check).
  2. The top stack item is less than 0 (negative time locks don’t make sense).
  3. The nLocktime is measured in blocks, and the top stack item uses seconds, or vice versa (apples and oranges).
  4. The top stack item is greater than the transaction’s lock_time (not enough time has passed).
  5. The nSequence field of this input is set to 0xFFFFFFFF (timelock might be disabled).

OP_CLTV replaces OP_NOP2, which (as you might expect) did nothing. Designing OP_CLTV to replace OP_NOP2 as a softfork provided an interesting constraint: OP_CLTV must leave the stack exactly as it found it. Because of this OP_CLTV reads a stack item, but does not consume a stack item. It checks the time lock, but then leaves the target time on the stack. As such, it is almost always followed by OP_DROP, which drops the top stack item.

Comparing the lock time specified in the script to the lock time of the transaction is a wonderfully clever implementation because the time is checked only indirectly. It passes enforcement to the nLocktime consensus rules while still allowing scipts to specify multiple different time-locked conditions. It allows scriptsig validity to be checked at any time and cached. The The downside is that if OP_CLTV is used in the script, lock_time must be specified in the spending transaction, and a sequence_no less than 0xFFFFFFFF must be present in the input. This can be counterintuitive for new developers, so keep this in mind.

OP_CLTV examples:

# Most of the transaction is omitted. Using decimal for human readability.
# Using hex for sequence numbers due to the presence of flags.
# Anyone can spend, at or after block 506391
tx_1:
  lock_time: 506391
  input_1:
    sequence_no: 0xFFFFFFFE
    script:
      506391 OP_CHECKLOCKTIMEVERIFY OP_DROP
# This transaction is invalid:
# The lock_time is in blocks, and the CLTV is in seconds
# The sequence_no is 0xFFFFFFFF
tx_2:
  lock_time: 506391
  input_1:
    sequence_no: 0xFFFFFFFF
    script:
      1514764800 OP_CHECKLOCKTIMEVERIFY OP_DROP
# This transaction is invalid
# The top stack item is greater than the lock_time
tx_3:
  lock_time: 506391
  input_1:
    sequence_no: 0xFFFFFFFE
    script:
      600000 OP_CHECKLOCKTIMEVERIFY OP_DROP
# This transaction is valid at block 512462, but only if at least 32 * 512 seconds have passed since its previous output confirmed.
# A separate transaction could be constructed to spend the coins between 506391 and 512462
tx_4:
  lock_time: 512462
  input_1:
    sequence_no: 0x00400020
    script:
      506391 OP_CHECKLOCKTIMEVERIFY OP_DROP
# This transaction becomes valid at block 506321
# The script allows an alternate execution path using 2-of-2 multisig.
# A separate transaction can be created that will not be time locked.
tx_5:
  lock_time: 506321
  input_1:
    sequence_no: 0xFFFFFFFE
    scriptsig:
      OP_TRUE
    script:
      OP_IF
        506321 OP_CHECKLOCKTIMEVERIFY OP_DROP
      OP_ELSE
        OP_2 <pubkey_1> <pubkey_2> OP_2 OP_CHECKMULTISIG
      OP_ENDIF
# This is a variation of an HTLC.
# This transaction is valid at block 507381 assuming:
# 1. The secret for input_2's script matches the expected secret hash
# 2. Valid signatures and pubkeys are provided for input_2
# 3. input_2's nSequence time-lock is respected.
tx_6:
  lock_time: 507381
  input_1:
    sequence_no: 0xFFFFFFFE
    script:
      507381 OP_CHECKLOCKTIMEVERIFY OP_DROP
  input_2:
    sequence_no: 0x000000A0
    scriptsig:
      <signature> <pubkey> <secret>
    script
      OP_HASH160 <secret hash> OP_EQUALVERIFY
      OP_DUP OP_HASH160 <pub keyhash> OP_EQUALVERIFY OP_CHECKSIGOP_CSV

OP_CHECKSEQUENCEVERIFY (OP_CSV) is the script-level relative time lock. It was described in BIP 112 and softforked in along with nSequence and MTP measurement in mid-2016.

Functionally, OP_CSV is extremely similar to OP_CLTV. Rather than checking the time, it compares the top stack item to the input’s sequence_no field. OP_CSV parses stack items the same way nSequence interprets lock-times. It respects nSequence’s disable flag and type flag, and reads 16-bit lock duration specifications from the last 16 bits of the stack item. OP_CSV errors if:

  1. The stack is empty (there’s no lock time specified).
  2. The top stack item is less than 0 (negative time is silly).
  3. The top stack item’s disable flag is not set and at least one of the following is true:
  • The transaction version is less than 2 (transaction does not signal OP_CSV compatibility).
  • The input’s sequence_no disable flag is set, (relative locktime is disabled).
  • The input’s sequence_no and top stack item’s type flags are not the same (not using the same metric).
  • The top stack item’s 16-bit duration is longer than the duration found in the input’s sequence_no field (not enough time has elapsed).

OP_CSV replaces OP_NOP3, and (like OP_CLTV) must leave the stack unmodified when it executes to maintain compatibility with older clients. It reads the top stack item, but does not consume it. So again it is often paired with OP_DROP. If the disable flag of the top stack item is set OP_CSV behaves as OP_NOP3.

As described earlier when discussing relative lock-times, OP_CSV is an amazing tool for stringing together chains of transactions. If we used OP_CLTV instead, the entire transaction chain would have an absolute expiration date. OP_CSV allows us to set an expiration date relative to the first broadcast transaction. So a chain of transactions can be made and stored indefinitely while maintaining time lock guarantees.

Transactions, once confirmed, cannot be revoked without a chain re-org. But chaining transactions via OP_CSV relative lock-times allows us to create script evaluation paths that almost provide that feature by creating mutually-exclusive future paths. Using OP_IF, we can construct multiple transactions spending the same previous output (which may itself be from an un-confirmed transaction), and ensure that one has a relative time-lock. Then, if the locked version be broadcast during its timelock, the unlocked version will confirm first and spend the coins. This means that we can give certain transactions priority over others, and control the execution of complex smart contracts. The Lightning network makes extensive use of this.

OP_CSV examples

# Most of the transaction is omitted. Using decimal for human readability.
# Using hex for sequence numbers due to the presence of flags.
# Anyone can spend, 255 blocks after the previous output confifrms.
tx_1:
  lock_time: 0
  input_1:
    sequence_no: 0x000000FF
    script:
      0x000000FF OP_CHECKSEQUENCEVERIFY OP_DROP
# Anyone can spend, so long as both of the following are true:
# a) 16,384 seconds have passed since input_1's previous output was confirmed
# b) 255 blocks have passed since input_2's previous output was confirmed
tx_2:
  lock_time: 0
  input_1:
    sequence_no: 0x00400020
    script:
      0x00400020 OP_CHECKSEQUENCEVERIFY OP_DROP
  input_2:
    sequence_no: 0x000000FF
    script:
      0x000000FF OP_CHECKSEQUENCEVERIFY OP_DROP
# Anyone can spend, so long as 256 blocks have passed since input_1's previous output.
# Note that a separate transaction can be created to spend these coins.
# The alternate path would specify a lock_time of at least 506321.
# The script allows either an absolute or relative time lock, whichever is shorter.
tx_3:
  lock_time: 0
  input_1:
    sequence_no: 0x00000100
    scriptsig:
      OP_FALSE
    script:
      OP_IF
        506321 OP_CHECKLOCKTIMEVERIFY
      OP_ELSE
        0x00000100 OP_CHECKSEQUENCEVERIFY
      OP_ENDIF
      OP_DROP
# This transaction is invalid until 1/1/2020,
# AND until 31457280 seconds after the previous output confirmed.
# It also specifies a single approved spender by their pubkey.
tx_4:
  lock_time: 1577836800
  input_1:
    sequence_no: 0x0004F000  # type flag is set
    scriptsig:
      <signature> <pubkey>
    script:
      1577836800 OP_CHECKLOCKTIMEVERIFY OP_DROP
      0x0004F000 OP_CHECKSEQUENCEVERIFY OP_DROP
      OP_DUP OP_HASH160 <pubkey hash> OP_EQUALVERIFY
      OP_CHECKSIGVERIFY
# This transaction is invalid 3 ways:
# 1) input_1's script fails because the stack item's 16-bit lock duration is greater than specified in the sequence_no.
# 2) input_2's script fails because the sequence_no's type flag is not set, while the stack item's type flag is set.
# 3) input_3's script fails because the stack is not empty at the end.
tx_5:
  lock_time: 0
  input_1:
    sequence_no: 0x0004F000
    script:
      0x0004FFFF OP_CHECKSEQUENCEVERIFY OP_DROP
  input_2:
    sequence_no: 0x0000FFFF
    script:
      0x0004FFFF OP_CHECKSEQUENCEVERIFY OP_DROP
  input_3:
    sequence_no: 0x00000001
    script:
      0x00000001 OP_CHECKSEQUENCEVERIFYReview

Bitcoin’s time locks are powerful tools, but can be confusing. Here’s a quick list of important things to remember:

  • OPs go in scripts.
  • “Locktime” means absolute.
  • “Sequence” means relative.
  • All time locks can do blocks or seconds, but they have different ways of signalling.
  • Don’t accidentally lock things for centuries.
  • Script-level time locks need a transaction-level lock of the same type in the spending tx.

Further Reading

 

from: https://medium.com/summa-technology/bitcoins-time-locks-27e0c362d7a1