Files
zeekayandHanzo Dev 2f1bfcd0f9 Let the precompile differential build, and say what it finds
The C++ evaluator named bls12_g1mul_analyze and bls12_g2mul_analyze. EIP-2537
folded multiplication into the multi-exponentiation precompiles before it was
finalised, so cevm carries neither symbol and the file had not compiled since.
A table that does not compile reports nothing, and this one was hiding a
second error underneath the first: it priced 0x0b through 0x11, cevm serves
nothing in that range at Cancun because EIP-2537 lands at Prague, and
check_table() exists to catch exactly that disagreement.

Nothing is priced there now, for the reason the file already gives for
p256verify: the revision has not reached it. The differential then runs, and
disagrees on 835 fields of 258 vectors. Both groups are real.

The C-Chain serves BLS at those addresses through Lux precompile modules
rather than through the EVM's revision-gated ones, which is why Go and Rust
answer where cevm does not; the C++ port has no equivalent module. And on
malformed input Go and Rust report a bad length where cevm reports out of gas.
Neither is a defect in the harness, and neither is fixed here.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:44:29 -07:00

350 lines
14 KiB
C++

// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// conformance.cpp — the C++ EVM's answers to the shared precompile corpus.
//
// One of the evaluators — Go, C++ — that read the same
// conformance/corpus/precompile_vectors.tsv and print the same five fields per
// vector. The runner compares them; this program never sees another
// implementation's answer and has nothing to agree with.
//
// The subject is cevm's own precompile seam: cevm::state::is_precompile and
// cevm::state::call_precompile. That pair is what the C++ node's EVM host
// calls (lib/evm/state/evmc_host.hpp, lib/evm/state/processor.cpp), so it is
// the price and the bytes a C++ validator would put in a state root. The GPU
// dispatcher beside it (lib/evm/gpu/precompiles) is an accelerator keyed on a
// sixteen-bit address that documents itself as matching this one; asking it
// would be asking the same table through a narrower window.
//
// Usage: evm_conformance <precompile_vectors.tsv>
#include <test/state/precompiles.hpp>
#include <test/state/precompiles_internal.hpp>
#include <evmc/evmc.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <vector>
using namespace cevm::state;
using namespace evmc::literals; // the _address literal, for whole addresses
namespace {
// The revision the C-Chain runs, which is the revision the other two
// evaluators answer at. cevm executes the C-Chain at Cancun (src/evm.cpp, and
// the unit tests say so), luxfi/evm maps its Quasar fork to CancunTime and has
// no OsakaTime for a network to set, and no cchain.json names a fork after
// Cancun. Asking at Osaka made this evaluator serve p256verify at 0x…0100,
// where the chain has an empty account, and price modexp by EIP-7883, which no
// validator charges.
constexpr evmc_revision kRevision = EVMC_CANCUN;
constexpr const char* kNone = "-";
// The verdict vocabulary, shared with the Go evaluator. There is no STATE here:
// no precompile cevm serves reads a chain.
constexpr const char* kOk = "OK";
constexpr const char* kFailed = "FAILED";
constexpr const char* kOog = "OOG";
constexpr const char* kAbsent = "ABSENT";
// ── the price of a call ──────────────────────────────────────────────────────
//
// GAS IS WHAT WAS CHARGED, NEVER WHAT WAS LEFT, and cevm's result carries what
// was left: call_precompile deducts analyze()'s gas_cost up front, hands back
// msg.gas - gas_cost on success, and zeroes even that on any non-success. The
// charge on a REFUSED call therefore survives nowhere in the result, and the
// only place it still exists is the analyze function that computed it.
//
// So this table is here — the same table precompiles.cpp dispatches on, read
// for the number rather than for the handler. A second copy of a table is a
// second thing to keep in step, and this one is kept in step by the compiler
// and by check_table() below rather than by whoever edits cevm next.
struct Price {
// The WHOLE address. cevm's dispatcher keys the stock range on the last two
// bytes and matches everything else on all twenty, and a Lux precompile is
// everything else — the inference body sits at 0x0300…0003, whose last two
// bytes are ripemd160's. Keying on two bytes here priced a call to that
// address at ripemd160's 720 gas and named it ripemd160, with no sign
// anything was wrong. No entry below needs the width today; the type keeps
// the next one from needing to discover this again.
evmc::address address;
const char* name;
decltype(identity_analyze)* analyze;
};
constexpr auto kPrices = std::to_array<Price>({
{0x0000000000000000000000000000000000000001_address, "ecrecover", ecrecover_analyze},
{0x0000000000000000000000000000000000000002_address, "sha256", sha256_analyze},
{0x0000000000000000000000000000000000000003_address, "ripemd160", ripemd160_analyze},
{0x0000000000000000000000000000000000000004_address, "identity", identity_analyze},
{0x0000000000000000000000000000000000000005_address, "expmod", expmod_analyze},
{0x0000000000000000000000000000000000000006_address, "ecadd", ecadd_analyze},
{0x0000000000000000000000000000000000000007_address, "ecmul", ecmul_analyze},
{0x0000000000000000000000000000000000000008_address, "ecpairing", ecpairing_analyze},
{0x0000000000000000000000000000000000000009_address, "blake2bf", blake2bf_analyze},
{0x000000000000000000000000000000000000000a_address, "point_evaluation", point_evaluation_analyze},
// Nothing is priced at 0x0b through 0x11 either. EIP-2537 lands at Prague
// and the revision above is Cancun, so cevm dispatches nothing in that
// range and a price here would be exactly the disagreement check_table()
// exists to catch. cevm does carry the seven analyzers, on the finalised
// layout — g1add, g1msm, g2add, g2msm, pairing_check, and the two field
// maps at the top — and they become priceable the revision the C-Chain
// reaches Prague, not before.
// Nothing is priced at 0x…0100 or at 0x0300…0003. cevm holds a p256verify
// and an aivm body and the chain reaches neither: p256verify arrives at
// Osaka, which is past the Cancun above, and aivm's aiInferenceConfig is
// in no upgrade.json, so it is in no dispatch table. check_table() below
// is what keeps that agreement — a price here for an address the seam does
// not serve fails the run.
});
// The widest address cevm's availability table can hold, so the scan below
// covers every address the seam could possibly answer for.
constexpr std::uint32_t kLastIndex = 0x0100;
evmc::address address_of(std::uint32_t index) noexcept
{
evmc::address a{};
a.bytes[18] = static_cast<std::uint8_t>(index >> 8);
a.bytes[19] = static_cast<std::uint8_t>(index);
return a;
}
const Price* price_of(const evmc::address& addr) noexcept
{
for (const auto& p : kPrices)
if (p.address == addr) return &p;
return nullptr;
}
// The two tables agree or this program does not run.
//
// An address cevm starts serving that has no price here would be priced by
// nothing on every refusal at it — a wrong number printed with the same
// confidence as a right one. An address priced here that cevm does not serve is
// the same mistake pointing the other way. Both are silent, and both stop
// being silent here.
bool check_table()
{
for (std::uint32_t i = 0; i <= kLastIndex; ++i) {
const auto addr = address_of(i);
const bool served = is_precompile(kRevision, addr);
const bool priced = price_of(addr) != nullptr;
if (served != priced) {
std::fprintf(stderr,
"evm_conformance: cevm %s a precompile at %04x and this evaluator %s it\n",
served ? "serves" : "does not serve", i, priced ? "prices" : "does not price");
return false;
}
}
// And every address this evaluator prices that the scan above cannot
// reach. A price for an address cevm does not serve is the same silent
// mistake as the reverse, and the loop's range is the stock one.
for (const auto& p : kPrices) {
if (!is_precompile(kRevision, p.address)) {
std::fprintf(stderr,
"evm_conformance: this evaluator prices %s at an address cevm does not serve\n",
p.name);
return false;
}
}
return true;
}
// ── the corpus ───────────────────────────────────────────────────────────────
struct Vector {
std::string id;
evmc::address address{};
std::uint64_t gas = 0;
std::vector<std::uint8_t> input;
};
int nibble(char c) noexcept
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
bool unhex(const std::string& s, std::vector<std::uint8_t>& out)
{
if (s == kNone) return true;
if (s.size() % 2 != 0) return false;
out.reserve(s.size() / 2);
for (std::size_t i = 0; i < s.size(); i += 2) {
const int hi = nibble(s[i]), lo = nibble(s[i + 1]);
if (hi < 0 || lo < 0) return false;
out.push_back(static_cast<std::uint8_t>((hi << 4) | lo));
}
return true;
}
std::string hex(const std::uint8_t* b, std::size_t n)
{
static const char* d = "0123456789abcdef";
std::string out;
out.reserve(n * 2);
for (std::size_t i = 0; i < n; ++i) {
out.push_back(d[b[i] >> 4]);
out.push_back(d[b[i] & 0xF]);
}
return out;
}
// ── the answer ───────────────────────────────────────────────────────────────
struct Row {
std::string id;
const char* status = kAbsent;
std::uint64_t gas = 0;
std::string output = kNone;
std::string note;
};
void print(const Row& r)
{
std::string note = r.note;
for (auto& c : note)
if (c == '\t' || c == '\n') c = ' ';
if (note.empty()) note = kNone;
std::printf("R\t%s\t%s\t%llu\t%s\t%s\n", r.id.c_str(), r.status,
static_cast<unsigned long long>(r.gas), r.output.c_str(), note.c_str());
}
Row run(const Vector& v)
{
Row r;
r.id = v.id;
if (!is_precompile(kRevision, v.address)) {
r.note = "no precompile at this address";
return r;
}
const Price* price = price_of(v.address); // non-null: check_table() said so
// A vector offering more gas than the seam can hold is a corpus this
// program cannot answer, and saying so beats truncating it into an answer.
if (v.gas > static_cast<std::uint64_t>(INT64_MAX)) {
std::fprintf(stderr, "evm_conformance: %s: gas %llu does not fit an int64\n", v.id.c_str(),
static_cast<unsigned long long>(v.gas));
std::exit(1);
}
// An empty input is a pointer nothing may be read through, not a null one.
static constexpr std::uint8_t kNoBytes[1]{};
evmc_message msg{};
msg.kind = EVMC_CALL;
msg.gas = static_cast<std::int64_t>(v.gas);
msg.recipient = v.address;
msg.code_address = v.address;
msg.input_data = v.input.empty() ? kNoBytes : v.input.data();
msg.input_size = v.input.size();
const auto charge = price->analyze({msg.input_data, msg.input_size}, kRevision).gas_cost;
const auto result = call_precompile(kRevision, msg);
r.note = std::string("cevm:") + price->name;
switch (result.status_code) {
case EVMC_OUT_OF_GAS:
// No charge to report: the call never happened, and a number nobody
// agrees to means the column stops comparing.
r.status = kOog;
r.note += ": out of gas";
return r;
case EVMC_SUCCESS:
// The seam's own arithmetic, which is the ground truth wherever the
// seam still has it.
r.status = kOk;
r.gas = v.gas - static_cast<std::uint64_t>(result.gas_left);
if (result.output_size > 0) r.output = hex(result.output_data, result.output_size);
// And where the seam still has it, it is what the price table says. A
// price wired to the wrong precompile passes the presence check above
// and prices every refusal at this address wrongly; here it does not
// survive the first call that succeeds.
if (r.gas != static_cast<std::uint64_t>(charge)) {
std::fprintf(stderr,
"evm_conformance: %s: the seam charged %llu and %s_analyze says %lld\n",
v.id.c_str(), static_cast<unsigned long long>(r.gas), price->name,
static_cast<long long>(charge));
std::exit(1);
}
return r;
default:
// The charge stands. A precompile that read the input and refused it
// did the work of reading it, and the result no longer carries what
// that cost, so it comes from the analyze that computed it.
r.status = kFailed;
r.gas = static_cast<std::uint64_t>(charge);
r.note += std::string(": ") + evmc::to_string(result.status_code);
return r;
}
}
} // namespace
int main(int argc, char** argv)
{
if (argc < 2) {
std::fprintf(stderr, "usage: evm_conformance <precompile_vectors.tsv>\n");
return 2;
}
if (!check_table()) return 1;
std::ifstream in(argv[1]);
if (!in) {
std::fprintf(stderr, "evm_conformance: cannot read %s\n", argv[1]);
return 1;
}
std::string line;
while (std::getline(in, line)) {
if (line.empty() || line[0] == '#') continue;
std::vector<std::string> f;
std::size_t start = 0;
for (;;) {
const std::size_t tab = line.find('\t', start);
if (tab == std::string::npos) {
f.push_back(line.substr(start));
break;
}
f.push_back(line.substr(start, tab - start));
start = tab + 1;
}
if (f.size() != 5 || f[0] != "V") {
std::fprintf(stderr, "evm_conformance: not a vector line: %s\n", line.c_str());
return 1;
}
Vector v;
v.id = f[1];
std::vector<std::uint8_t> addr;
if (!unhex(f[2], addr) || addr.size() != sizeof(v.address.bytes)) {
std::fprintf(stderr, "evm_conformance: %s: not a 20-byte address: %s\n", v.id.c_str(),
f[2].c_str());
return 1;
}
for (std::size_t i = 0; i < addr.size(); ++i) v.address.bytes[i] = addr[i];
v.gas = std::strtoull(f[3].c_str(), nullptr, 10);
if (!unhex(f[4], v.input)) {
std::fprintf(stderr, "evm_conformance: %s: input is not hex\n", v.id.c_str());
return 1;
}
print(run(v));
}
return 0;
}