Skip to content

DTVM EVM: block_timestamp and block_number silently wrap to negative int64_t for values in [2^63, 2^64-1] #564

Description

@erxiaozhou

1. Problem description

DTVM's --load-state parser (src/utils/evm.cpp, loadState()) validates block_timestamp and block_number with IsUint64() but assigns the result to int64_t fields:

// src/utils/evm.cpp:421-424  (block_number)
if (TxContext.HasMember("block_number") &&
    TxContext["block_number"].IsUint64()) {
  Host.tx_context.block_number = TxContext["block_number"].GetUint64();
}

// src/utils/evm.cpp:426-430  (block_timestamp)
if (TxContext.HasMember("block_timestamp") &&
    TxContext["block_timestamp"].IsUint64()) {
  Host.tx_context.block_timestamp =
      TxContext["block_timestamp"].GetUint64();
}

IsUint64() accepts values up to 2^64-1, but evmc_tx_context::block_number and evmc_tx_context::block_timestamp are both int64_t (max 2^63-1). For inputs in [2^63, 2^64-1], the IsUint64() check passes, GetUint64() succeeds, and the implicit uint64_t -> int64_t assignment silently wraps the value to negative.

The EVMC ABI (evmc.h) defines both fields as int64_t:

struct evmc_tx_context {
    ...
    int64_t block_number;     /**< The block number. */      // evmc.h:219
    int64_t block_timestamp;  /**< The block timestamp. */   // evmc.h:220
    ...
};

The output serialization (src/utils/evm.cpp:278-280) writes the raw int64_t value directly as a bare decimal integer, producing invalid (negative) JSON values:

File << "    \"block_number\": " << Host.tx_context.block_number << ",\n";
File << "    \"block_timestamp\": " << Host.tx_context.block_timestamp << ",\n";

This is the same bug pattern as block_gas_limit (documented separately in POCS/dtvm_gas_limit_parsing.md), now confirmed to also affect block_timestamp and block_number.

Impact

block_timestamp

Input block_timestamp IsUint64 Stored int64_t value TIMESTAMP opcode return result.json output
1000 1000 0x0000…03E8 1000 ✓
2^63-1 (9223372036854775807) 9223372036854775807 0x7fff…ffff 9223372036854775807 ✓
2^63 (9223372036854775808) -9223372036854775808 0x8000…0000 (accidentally correct) -9223372036854775808
2^64-1 (18446744073709551615) -1 0xffff…ffff (accidentally correct) -1
2^64 (18446744073709551616) 0 (silent default) 0x0000…0000 0 ✗

The TIMESTAMP opcode (0x42) accidentally returns the correct value because intx::uint256(int64_t) reinterprets via implicit uint64_t conversion, undoing the truncation. However, the stored block_timestamp is negative, which is semantically incorrect and would break any int64_t-level comparison or downstream consumer that reads result.json.

block_number

Input block_number IsUint64 Stored int64_t value NUMBER opcode return result.json output
1 1 0x0000…0001 1 ✓
2^63 (9223372036854775808) -9223372036854775808 0x8000…0000 (accidentally correct) -9223372036854775808

Same pattern: NUMBER opcode (0x43) accidentally returns correct bits, but result.json reports a negative block number.

Minimal reproduce step

Reproduce 1: block_timestamp = 2^63 (negative wrap)

Contract bytecode (contract_ts.evm.hex) — calls TIMESTAMP and returns the result:

425f5260205ff3

Disassembly: TIMESTAMP PUSH0 MSTORE PUSH1 0x20 PUSH0 RETURN

state.json (sets block_timestamp to 2^63, which passes IsUint64()):

{
  "accounts": {
    "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
      "balance": "000000000000000000000000000000000000000000000000000000ffffffffff",
      "nonce": 0, "code": "0x", "storage": {}
    },
    "00000000000000000000000000000000000000f1": {
      "balance": "0000000000000000000000000000000000000000000000000000000000000000",
      "nonce": 0, "code": "0x425f5260205ff3", "storage": {}
    }
  },
  "tx_context": {
    "gas_price": "0000000000000000000000000000000000000000000000000000000000000010",
    "block_number": 1,
    "block_timestamp": 9223372036854775808,
    "block_coinbase": "b94f5374fce5edbc8e2a8697c15331677e6ebf0b",
    "block_prev_randao": "0000000000000000000000000000000000000000000000000000000000200000",
    "block_gas_limit": 10944489199640098,
    "block_base_fee": "0000000000000000000000000000000000000000000000000000000000000010"
  }
}

Run:

dtvm --enable-evm-gas --format evm -m multipass --evm-revision cancun \
  --load-state state.json \
  --sender a94f5374fce5edbc8e2a8697c15331677e6ebf0b \
  --gas-limit 8000000 \
  --contract-address 00000000000000000000000000000000000000f1 \
  --save-state result.json \
  contract_ts.evm.hex

Verified output:

output: 0x0000000000000000000000000000000000000000000000008000000000000000

result.json contains:

    "block_timestamp": -9223372036854775808,

The TIMESTAMP return value's bit pattern is accidentally correct (0x8000…0000 = 2^63), but block_timestamp in result.json is negative.

2. What did you expect to see

For block_timestamp and block_number values that pass IsUint64() but exceed INT64_MAX, DTVM should either:

  • Reject the input with an error (since evmc_tx_context::block_timestamp and block_number are int64_t, values > 2^63-1 are out of range), or
  • Clamp to INT64_MAX.

For values that exceed uint64_t (>= 2^64), DTVM should report a parse error rather than silently defaulting to 0.

At minimum, the stored values should never be negative, and the TIMESTAMP/NUMBER opcodes should never silently return 0 for a non-zero configured input.

3. What did you see instead

DTVM accepted the input without any warning:

  • For values in [2^63, 2^64-1]: result.json reported negative block_timestamp / block_number.
  • For values >= 2^64: result.json reported 0 and the TIMESTAMP opcode returned 0.

4. Error logs / Stack trace

No error output at any log level. DTVM silently produces the incorrect values.

5. What is the version

commit 5d64911f76bfe0b47fdc958a07bdcafd9e9d5343

Both interpreter and multipass builds are affected (verified with -m interpreter and -m multipass).

6. Environment

  • OS: Ubuntu 20.04.6 LTS, Linux 6.8.0-111-generic x86_64
  • GCC 11.4.0, Clang 10.0.0-4ubuntu1, LLVM 20.1.5 at /usr/lib/llvm-20, CMake 3.28.1
  • Build: bash build_evm_interpreter.sh / bash build_evm_multipass.sh

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions