Monero timelock vulnerabilities
TLDR: Monero, and in general cryptonote based cryptocurrencies, transaction unlock_time values were interpreted with local time, allowing a host of non-critical exploits targeting the integrity of the blockchain. The issue is patched as of monero version 0.17.0.0. No harm towards user funds was found related to the issue.
Next to containing code activating the new CLSAG signature algorithm, the v0.17.0.0 release of monero also contains a security patch. The patch was called “Deterministic Unlock Times” in the release notes. The reason for this new functionality was not provided before, which led to some speculation on its nature. It is now given both in this article and the HackerOne disclosure. This is the second article in my monero unlock time series. The first article is here.
Let’s quickly recap what a monero transaction roughly contains. Similarly to bitcoin, monero transactions have inputs and outputs. Outputs represent the amounts potentially available for further spending, inputs represent the now consumed outputs from a previous transaction. Additionally, a monero transaction selects as an input from n previous transaction outputs (ring members) and proves with a ring signature that the author of the transaction owns one of them. The transaction author then constructs new outputs from these inputs and cryptographically proves that the amounts balance.
The unlock_time field in a monero transaction dictates when coins in a transaction can be spent again, or more exactly when a transaction’s outputs can be spent again. No transactions can use these outputs until the timelock has timed out. Money transferred in time-locked transactions is only spendable after a certain time.
The rule is enforced by the consensus code in
and has two parts. If the
unlock_time is below 500’000’000, it is interpreted
as a block height and compared with the current block height. If the
unlock_time in a used transaction input’s previous output is below the
current block height, a transaction using that output is valid. Otherwise, it
is invalid and not relayed.
When the unlock time is above 500’000’000, it is interpreted as Unix time in
seconds rather than block height. Surprisingly these time-based
were not compared to the network or rather block time, but to the local time of
the machine running the monero node. Using this local time is problematic.
Nodes should use as much shared blockchain data as possible when verifying
transactions, or concisely: Consensus rules must react to consensus
variables. Since time varies from machine to machine, it is not a consensus
I shared this issue with monero research lab regular contributor Isthmus (@Mitchellpkt0 on twitter) and we explored the following vulnerabilities. I do consider them serious enough to have warranted a report and some degree of secrecy, but I don’t believe them to be critical, nor particularly dangerous.
By consuming outputs locked with carefully chosen
unlock_time values, it is
possible to poll the local time of other nodes. The attacker can generate any
number of transactions encumbered with a spread of
unlock_time values, for
example with an
unlock_time 20 blocks per the expected block time into the
future and then spread out over intervals of seconds. The attacker then creates
transactions consuming these locked outputs and connects two nodes to the node
she wants to spy on. She sends the transaction from one of the nodes to the
node she wants to spy on and then checks if the other node receives the
transaction. If the consumed
unlock_time is invalid by the consensus rules for
the attacker and the greater network, but valid for the surveilled node, the
attacker will get the transaction relayed to his other node as well. This then
indicates the local time on the surveilled node matching the time on the
attacker’s transaction. Using a binary search methodology, the attacker can
then pinpoint the node’s time within a precision of seconds.
If such a node is found with a different clock (running late or early), or if
the attacker can manipulate a node’s local time through another channel, we
identified two additional ways to exploit this
Assume a node has a clock running forward (having a higher Unix time). The
attacker creates a transaction consuming at least one output with a high enough
unlock_time such that it is invalid by the clocks of most nodes on the
network, but valid for the node where the clock runs forward. If the attacker
relays this transaction to the victim’s node, the node will validate it as a
valid transaction. This is especially useful for the attacker in the context of
mining. For example, if the victim’s node is a large mining pool, this can be
used to make the pool expend work on a block that is not valid with the locked
transaction included by the rules of the rest of the network. If the
attacker is a mining pool herself, she can use this to increase her profit.
If a node has a clock running behind (showing a lower Unix time), transactions
consuming outputs encumbered by a recently expired
unlock_time are valid and
minable for most nodes on the network, but not for nodes with a lower local
Unix time. In fact, any nodes with a lower local Unix time will reject any
transactions and blocks containing transactions spending outputs that are still
locked from their point of view. Explaining this in words may be confusing:
Assume someone creates a transaction spending an output encumbered with
unlock_time 1500000020, while the actual current Unix time is 1500000040.
Monero nodes running on a system with the correct actual Unix time will accept
this transaction since the
unlock_time has expired. Nodes with a lower local
Unix time, say 1500000000 will not accept this transaction. From their
unlock_time is not expired, rendering the transaction
An attacker can continuously submit transactions consuming an
encumbered output with a value just later than the victim node’s clock,
ensuring the node will never catch up to the tip of the network’s best chain.
This could be used for a short term eclipse attack, even potentially allowing
the attacker enough time to feed it a “slower” malicious chain. Once the
node’s time has eventually surpassed the
unlock_time, the node can accept the
block containing the time-locked transaction from the best chain again. Since
connections with peers are dropped if they feed ‘invalid’ blocks, there is no
telling for how long exactly the attack could be sustained, because the node
might first have to accept connections with its peers again. This form of
eclipse attack could be used against an exchange (if the exchange node’s clock
is wrong) to double spend.
Some Mitigating Factors and Data
To assess the risk of this happening without a further exploit enabling NTP time or machine time manipulation, I produced the following graph:
It shows the timestamp difference between two successive blocks. Negative time indicates at least one of the two blocks having a block timestamp differentiating from the actual time. Blocks produced in the past cannot appear after new blocks! We can use all these negative differences as a rough estimate, bar any real understanding of the statistical underpinning, for blocks produced by a machine with a significant time discrepancy. This amounts to about 2.6% of all produced blocks, which gives us a guestimate of vulnerable miners to the above exploit without further manipulation.
Following the discovery, we submitted a report to monero’s HackerOne outlining the above problems and containing a potential patch for the issue.
A similar problem used to exist in bitcoin’s
nlocktime field. However, this
problem was less serious at the time, since the
nlocktime semantics are
different compared to monero’s
nlocktime's influence is on the
finality of the transaction itself, not a later transaction), and since the
last block’s timestamp was used instead of the local time. It was fixed through
now uses the median timestamp of the past 11 blocks to determine if an unlock
time is valid. The fix was also a required step for the deployment of the CSV
and CLTV opcodes, which adopt a similar output locking mechanism to the one of
The newest version of monero contains a change in the consensus rules making
the Unix time-based
unlock_time dependent on the timestamp of previous blocks.
The original patch was much inspired by how bitcoin handles its time-based
unlock time values. It took the median block timestamp of the past 60 blocks
and compared it to the unlock time value.
This patch was then expanded upon by other reviewers until settling for the formula
min(median(block timestamps of the past 60 blocks) + 61 minutes,
current tip block timestamp + 2 minutes)
It aims to more accurately represent the current time.
The full contents and discussion are available on GitHub. These new validation rules were activated with the latest monero hardfork together with the CLSAG transaction signature and format validation.
Closing notes on the monero code
Interestingly the code already contained a function
just returned the local time of the system running monerod. It did have some
ominous comments indicating this was not its intended form.
The function has now been repurposed to calculate this actual “adjusted” time.
I found this vulnerability while reading through the TODO comments in the code. Monero inherited a largely uncommented source code from the original cryptonote developers. Particularly hazy areas are commented with either //TODO or //FIXME. I would strongly recommend other security researchers to take a tour through these. There are roughly 120 TODOs and 30 FIXMEs left, so there may be more to find.