Economical Tezos denial of service attack

Mike Radin, 2022/02/13

Brief

Tezos is a highly decentralized blockchain with a Turing-complete programming language for smart contracts. This article describes how to delay execution of operations on the chain for arbitrarily long periods with a possibility of economic incentive to do so.

Block Construction

Tezos has an on-chain governance mechanism that allows the chain to be upgrade to receive new technical and economic features with minimal political drama. One such upgrade reduced block duration to 30 seconds and modified total block gas cap to 5,200,000 units. The default mechanism by which operations are selected by validators for inclusion in the block is based on profitability. More or less that means the operation with the highest fee in the mempool is the most-likely to be included in the block. More specifically the parameters that go into the prioritization decision are fee-to-gas ratio. Hence the operation with the highest fee relative to its requested gas limit will appear at the top of the block. The node will then attempt to fill the rest of the block with operations sorted by this ratio. One more thing is that Tezos has a concept of operation group where a single submission from an account can contain more than one operation. Operations within the group will be ordered as submitted by the user. Each individual operation must have its own gas and storage limits. Operation group fee can be aggregated into the first operation in that group. There is a gas limit cap of 1,040,000 units per operation.

What this means is that we can construct an operation group with at least 5 operations to soak up all available gas in the block and we can price it in such a way that the validator will accept it and will have no more room for any other operation group within the block.

Note that the operations in question can utilized a small fraction of the gas limit they ask for, but they will be prioritized based on fee-to-gas-limit ratio. Additionally, each operation within the group must have the correct counter set and the operation group must be submitted with a valid "branch". A branch is a recent block hash, one that occurred within the last 64 blocks. This is also the mechanism for timeing out operations from the mempool, meaning that if an operation group is submitted with a block that was included 54 blocks ago, then this operation will only be pending for 10 more blocks before being dropped.

Blueberry Jam - A Proof of Concept

To test this idea an operation group was constructed to call a random smart contract on the chain and append four self-transfer operations to take up the remaining 4M gas units. This operation was executed in block 2107813.

[
  {
    "destination": "KT1XBVBog1Gj92ynwKK3qQzRfRk6ih7KKxgJ",
    "amount": "0",
    "storage_limit": "8",
    "gas_limit": "1039980",
    "counter": "16466670",
    "fee": "988000",
    "source": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1",
    "kind": "transaction",
    "parameters": { "entrypoint": "default", "value": { "string": "blueberry" } }

  },
  { "destination": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "amount": "1", "storage_limit": "0", "gas_limit": "1039950", "counter": "16466671", "fee": "0", "source": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "kind": "transaction" },
  { "destination": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "amount": "1", "storage_limit": "0", "gas_limit": "1039950", "counter": "16466672", "fee": "0", "source": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "kind": "transaction" },
  { "destination": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "amount": "1", "storage_limit": "0", "gas_limit": "1039950", "counter": "16466673", "fee": "0", "source": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "kind": "transaction" },
  { "destination": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "amount": "1", "storage_limit": "0", "gas_limit": "1039950", "counter": "16466674", "fee": "0", "source": "tz1eU1NCk7U6sNxDKos7CJMM7P97bW3wyGB1", "kind": "transaction" }
]

As you can see only one operation group appears in that block and the gas consumed to gas limit ratio is very small.

A Novel NFT

Tezos has two types of storage. There is contract storage, plain and big-map, to make data available across blocks. This storage currently has a fee of 0.00025 XTZ per byte. Alternatively it's possible to store data within the operation itself at a much lower fee per byte. This opens an opportunity to persist data that can be extracted by an indexer or an API, though not used within a smart contract across blocks (at this time). We can then construct a smart contract that will accept parameters in a certain format with no logic and no persistent storage. We can craft these parameters to store an image in multiple parts across multiple transactions as bytes. A special application could then reconstitute this image for display.

Economical Denial of Service

Some time ago one of the Tezos ecosystem developers deployed a social experiment on chain that allows people to call a contract to become a winner if nobody else calls that contract during the countdown. The countdown is adjusted by a small fraction at every call. At the time of writing the timer was 10 hours and 34 minutes with a winnable pot of 964.8 XTZ. This duration would, under normal circumstances, contain 1,268 blocks. The base fee/gas limit ratio is 0.1, meaning we should expect to spend at least 0.52 XTZ per block to buy up all the gas. In practice due to arbitrage bot competition and un-optimized interface libraries implied gas cost is higher. We can see what it might be by comparing total gas limit and fee across some number of recent blocks.

At the time of writing, the ratio of 0.18 was likely to work. It would be possible to derive this ratio in real time by monitoring the mempool for incoming operations. At this ratio we would be paying 0.936 XTZ per block to prevent anyone else from executing operations. This is unlikely to hold constant. There are bots arbitraging DeFi projects on Tezos, blocks with their operations will be priced higher. Presumably there are bot monitoring the tzbutton contract to try to win the pot as well. However this does provide a possiblity of winning 964.8 XTZ allowing 0.76 XTZ per block. As this pot continues to grow, the per-block cost will improve.

If the intent was simply to win the tzbutton pot, it would be possible to write a bot monitoring for operations against the `KT1H28iie4mW9LmmJeYLjH6zkC8wwSmfHf5P` contract and simply bump them off from the block by manipulating the fee/gas ratio.

Jam

On Feb 11, 2022, 1 XTZ was around 4 USD, or the cost of a jar of jam.

Appendix 1, Reproducibility

Running this script a few days later we get "raspberry" jam in block 2113497 and then "cranberry" jam in block 2490791 with transactions ooerK...Y8pz and ooyCC...CrZf respectively.