Skip to content

feat: per-contract explorer parser modules#2044

Draft
Maxnflaxl wants to merge 2 commits into
boost-1.90from
feat/explorer-modules
Draft

feat: per-contract explorer parser modules#2044
Maxnflaxl wants to merge 2 commits into
boost-1.90from
feat/explorer-modules

Conversation

@Maxnflaxl
Copy link
Copy Markdown
Member

@Maxnflaxl Maxnflaxl commented May 3, 2026

feat: per-contract explorer parser modules

Summary

Replaces the single monolithic bvm/Shaders/Explorer/Parser.cpp (~3.5k LOC, one giant switch over every contract SID) with a folder of small per-contract parser wasm modules that the explorer node loads at startup and dispatches by SID.

Each contract now ships its own parser.cpp next to contract.h / app.cpp, exporting a 4-method ABI (Method_0/1/2 mirror the old monolith entrypoints; Method_3 is new — it returns the SIDs the module claims). The explorer compiles every *.wasm in the configured folder, asks each module which SIDs it handles, builds a SID → bytecode map, and routes parse calls per-SID. Modules unknown to the explorer are simply skipped (parse-only feature, no consensus impact).

Motivation

  • Maintainability: the monolith was a single 3.5k-line file that touched every contract's internals; adding or changing a contract meant editing it. Now each contract owns its own parser alongside its other artifacts.
  • Independent versioning: versioned contracts can ship every entry of their s_pSID[] from one module and recover iVer internally — no more central registry.

Background: why the regular node ever had a parser at all

In master, --contract_rich_parser <file> was registered on both the main beam-node binary (beam/cli.cpp) and explorer-node (explorer/explorer_node.cpp), with identical wiring on each side: read the path, bvm2::Processor::Compile(..., Kind::Manager), stash the bytes in node.m_Cfg.m_ProcessorParams.m_RichParser, and set RichInfo::UpdShader. That last bit triggered persistence inside NodeProcessor::Initialize:

if (StartParams::RichInfo::UpdShader & sp.m_RichInfoFlags)
{
    m_DB.ParamSet(NodeDB::ParamID::RichContractParser, nullptr, &sp.m_RichParser);
    if (!bRebuildNonStd && m_DB.ParamIntGetDef(NodeDB::ParamID::RichContractInfo))
        bRebuildNonStd = true;
}

So the compiled blob was written into the node DB itself, under ParamID::RichContractParser, and any subsequent rich-info parsing (ProcessorInfoParser::Init in node/processor.cpp) read it straight back out:

m_Proc.m_DB.ParamGet(NodeDB::ParamID::RichContractParser, nullptr, nullptr, &m_bufParser);

That meant operators only had to pass --contract_rich_parser once (or whenever they wanted to swap the shader); both beam-node and explorer-node could install or replace it, and either binary would dutifully run it on every contract invocation during block interpretation.

…but the regular node never consumed the output

A close reading of master shows that the parser machinery in the node was always producer-only:

  • BlockInterpretCtx::BvmProcessor::ParseExtraInfo runs the parser on every contract invocation and writes the resulting string into ContractInvokeExtraInfo::m_sParser.
  • NodeProcessor::get_ContractDescr runs it on demand for a given sid/cid.

But every consumer of m_sParser and get_ContractDescr lives in explorer / JSON-RPC plumbing (the contract-info RPC, extract_contract_invoke_extra_info, etc.). Nothing in node/, core/, pow/, the wallet, or the P2P/consensus paths reads either field. A beam-node started with --contract_rich_parser but no explorer-style client on top would happily compile the shader, persist it in NodeDB, and run it on every block — and then throw the result away.

The reason it lived on the node side was purely mechanical: rich info is generated during block interpretation, which is a node responsibility, and persisting the parser in NodeDB saved operators from re-passing the shader on every restart. Semantically it was always an explorer concern.

That history is what makes this branch's split clean: lifting the parser map into the explorer process and dropping the DB row removes a feature path that no node-only deployment ever benefited from.

Changes

Host (explorer + node)

  • explorer/explorer_node.cpp
    • Replaces --contract_rich_parser <file> with --contract_rich_parser_folder <dir>.
    • Adds --contract_rich_parser_folder_dryrun: scans the folder, prints the SID → module map, exits.
    • Two-phase load: load_parser_modules() reads + compiles every *.wasm at CLI-parse time (deterministic order); apply_parser_modules() queries Method_3 after node.Initialize() to discover SIDs and refuses to start on duplicate SID claims across modules.
  • node/processor.{h,cpp}
    • New NodeProcessor::m_RichParserModules (map<ShaderID, ByteBuffer>), populated by the explorer before Initialize().
    • ProcessorInfoParser::Init() now takes a SID and looks up the module in the map; returns false (skip) if no module is registered for that SID.
    • Removes the DB-stored single-shader pathway: m_Cfg.m_ProcessorParams.m_RichParser, StartParams::RichInfo::UpdShader, and the ParamSet/ParamGet of NodeDB::ParamID::RichContractParser are all gone. ProcessorInfoParser no longer carries m_bufParser either — bytecode is borrowed from the in-memory map.
    • New ParserModule_GetSupportedSids() runs the two-call Method_3 protocol against a compiled module and returns its SID list.
  • beam/cli.cpp, utility/cli/options.{h,cpp} — drop the CONTRACT_RICH_PARSER parsing block from beam-node entirely. The string constant cli::CONTRACT_RICH_PARSER is retained because the explorer's option registration still uses it; no other binary references it.

DB migration (implicit)

  • NodeDB::ParamID::RichContractParser is no longer read or written anywhere. The enum value is kept (no renumbering), and NodeDB::Open already runs ParamDelSafe(ParamID::RichContractParser) from a prior schema migration, so any leftover row from old beam-node runs is silently dropped on next open.
  • No explicit migration step is required; existing node DBs continue to work, they just stop carrying a parser blob they never used in a node-only configuration.

Shaders

  • bvm/Shaders/Explorer/parser_module_abi.h — defines the 4-method module ABI, the two-call Method_3 SID-discovery protocol, and convenience macros (PARSER_MODULE_EXPORT_SIDS, PARSER_MODULE_EXPORT_KIND_ONLY).
  • bvm/Shaders/Explorer/parser_common.h — shared Doc* helpers extracted from the monolith.
  • Per-contract parser.cpp added under each contract dir (amm, bans, blackhole, dao-accumulator, dao-core, dao-core2, dao-vault, dao-vote, faucet, faucet2, gallery, minter, nephrite, oracle2, pbft, sidechain_pos, vault, vault_anon).
  • Compiled *.parser.wasm artifacts checked in under bvm/Shaders/Explorer/modules/ so explorers can run without a local toolchain.
  • bvm/Shaders/Explorer/Parser.cpp / Parser.wasm deleted.

Build tooling

  • make_all.sh / make_shader.sh — adds an --export [out_dir] mode that builds everything and collects */parser.wasm into Explorer/modules/ (default), ready to be pointed at by --contract_rich_parser_folder.

Operator notes / breaking changes

  • Flag rename, not backwards-compatible: explorer configs using --contract_rich_parser <file> must switch to --contract_rich_parser_folder <dir>. There is no auto-fallback to the old single-file form.
  • beam-node no longer accepts --contract_rich_parser at all. As described above, parsing was already an explorer-only concern in practice; the flag is removed from the main node binary and rich parsing now requires running explorer-node.
  • DB migration is implicit: the old NodeDB::ParamID::RichContractParser row is ignored on read; nothing reads or writes it anymore. Operators do not need to wipe or migrate node DBs.
  • Unknown / unhandled SIDs are a no-op, not an error: contract invocations whose SID has no registered parser module simply produce no rich info, exactly as if --contract_rich_parser_folder were unset.
  • Duplicate SID claims fail loudly: if two modules in the folder both claim the same SID via Method_3, apply_parser_modules() refuses to start the explorer with a clear error — easier to diagnose than the previous monolith's silent-precedence behavior.

Test plan

  • bvm/Shaders/make_all.sh --export produces the expected set of modules in Explorer/modules/.
  • Explorer starts with --contract_rich_parser_folder bvm/Shaders/Explorer/modules and serves rich info for at least one tx of each shipped contract (amm, nephrite, dao-vote, gallery, faucet/faucet2, vault/vault_anon, bans, blackhole, sidechain_pos, pbft, oracle2, minter, dao-* family).
  • --contract_rich_parser_folder_dryrun prints a SID → module map and exits 0.
  • Two modules claiming the same SID cause startup to fail with a clear error.
  • Empty / missing folder is a no-op (explorer starts, parse calls return without rich info).
  • Main beam-node still builds and runs; no surprises from removing m_RichParser from StartParams.
  • An existing node DB that had ParamID::RichContractParser written by an older beam-node opens cleanly under the new build (the row should be dropped silently on first open).
  • node_test passes (it's been heavily reformatted in this branch — verify it actually still tests what it did before).

@Maxnflaxl Maxnflaxl changed the title Feat/explorer modules feat: per-contract explorer parser modules May 3, 2026
@Maxnflaxl Maxnflaxl changed the base branch from master to boost-1.90 May 3, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant