Raised by mfw78 during nullislabs/shepherd#471 review. Follow-up, not a change to any car in flight.
A module's on-chain topic is stated twice: once in code, where the sol! declaration derives SIGNATURE_HASH and the decode guard matches on it, and once in module.toml as event_signature, which is what the host actually filters on (nexum-runtime::supervisor -> build_alloy_filter). Nothing structural keeps the two in step.
The idea: let a keeper or venue articulate in code which event it subscribes to, and derive the manifest fact from that, so the topic exists in exactly one place.
Evidence that the current split is not holding
twap-monitor carries a hand-written parity test, and until nullislabs/shepherd#471 it was a substring match over the manifest text that would have passed while the real event_signature had drifted. It now parses the manifest and compares a typed B256.
ethflow-watcher has no such test at all. It pins event_signature = "0xcf5f9de2..." in module.toml, matches OrderPlacement::SIGNATURE_HASH in its decode guard, and nothing verifies the two agree. A drift there is silent: the host subscribes to one topic, the guest decodes another, and the module simply never sees an event.
So the guard is per-module, optional, and was wrong in the one place it existed.
Precedent already in the tree
The macros already read module.toml at compile time. #[nexum_sdk::module] and #[videre_sdk::keeper] synthesize the per-module WIT world from [capabilities], fail the build with targeted errors, and register the manifest as a rebuild anchor so edits retrigger expansion.
There is also precedent for a code-versus-manifest contract that is currently reconciled by convention: cow-venue's adapter defines fn body_versions() whose comment reads "Must equal the manifest [venue] body_versions", with videre-host::handshake validating the manifest side at install.
Options
Compile-time parity, manifest stays authoritative. The macro reads the manifest's event_signature and fails the build unless it equals the topic-0 of an event named in the attribute. Cheapest, deletes the per-module test boilerplate, closes the ethflow gap by construction, and changes no host contract. Does not remove the duplication, only makes it impossible to get wrong.
Code declares, manifest is derived. The attribute names the event type, and the macro emits the subscription into an artefact the host reads, either a generated manifest fragment or a custom wasm section. This is the "everything in code" end state. Needs a decision on where the host reads it from and how cargo-built artefacts stay in step with a checked-in manifest.
Install-time export. The guest exports its subscription set and the host calls it before wiring filters, mirroring the existing venue handshake. Most faithful to "in code", but it moves a pre-instantiation fact behind guest execution and widens the WIT world.
The constraint that decides it
module.toml is the operator-auditable admission surface: today an operator can read exactly which chains, addresses and topics a module will be given without building the guest. Any design that moves subscriptions into guest code has to keep that property, whether by emitting a manifest the operator still reviews or by keeping host-side bounds on what a module may subscribe to. A subscription that is only visible by disassembling a wasm is a regression in auditability even if it is nicer to write.
That argues for the first option now, and the second only if the emitted artefact stays reviewable.
Timing
Cheaper before the M5 carve. It touches nexum-module-macros and nexum-runtime (L1), videre-macros (L2) and the modules (L3), which become three repositories afterwards.
Acceptance criteria
A drift between a module's declared topic and the topic the host filters on is caught mechanically, not by a per-module hand-written test.
ethflow-watcher is covered by whatever mechanism lands, since it is currently unguarded.
The operator can still determine a module's subscriptions without building it.
A module's on-chain topic is stated twice: once in code, where the
sol!declaration derivesSIGNATURE_HASHand the decode guard matches on it, and once inmodule.tomlasevent_signature, which is what the host actually filters on (nexum-runtime::supervisor->build_alloy_filter). Nothing structural keeps the two in step.The idea: let a keeper or venue articulate in code which event it subscribes to, and derive the manifest fact from that, so the topic exists in exactly one place.
Evidence that the current split is not holding
twap-monitorcarries a hand-written parity test, and until nullislabs/shepherd#471 it was a substring match over the manifest text that would have passed while the realevent_signaturehad drifted. It now parses the manifest and compares a typedB256.ethflow-watcherhas no such test at all. It pinsevent_signature = "0xcf5f9de2..."inmodule.toml, matchesOrderPlacement::SIGNATURE_HASHin its decode guard, and nothing verifies the two agree. A drift there is silent: the host subscribes to one topic, the guest decodes another, and the module simply never sees an event.So the guard is per-module, optional, and was wrong in the one place it existed.
Precedent already in the tree
The macros already read
module.tomlat compile time.#[nexum_sdk::module]and#[videre_sdk::keeper]synthesize the per-module WIT world from[capabilities], fail the build with targeted errors, and register the manifest as a rebuild anchor so edits retrigger expansion.There is also precedent for a code-versus-manifest contract that is currently reconciled by convention:
cow-venue's adapter definesfn body_versions()whose comment reads "Must equal the manifest[venue] body_versions", withvidere-host::handshakevalidating the manifest side at install.Options
Compile-time parity, manifest stays authoritative. The macro reads the manifest's
event_signatureand fails the build unless it equals the topic-0 of an event named in the attribute. Cheapest, deletes the per-module test boilerplate, closes the ethflow gap by construction, and changes no host contract. Does not remove the duplication, only makes it impossible to get wrong.Code declares, manifest is derived. The attribute names the event type, and the macro emits the subscription into an artefact the host reads, either a generated manifest fragment or a custom wasm section. This is the "everything in code" end state. Needs a decision on where the host reads it from and how
cargo-built artefacts stay in step with a checked-in manifest.Install-time export. The guest exports its subscription set and the host calls it before wiring filters, mirroring the existing venue handshake. Most faithful to "in code", but it moves a pre-instantiation fact behind guest execution and widens the WIT world.
The constraint that decides it
module.tomlis the operator-auditable admission surface: today an operator can read exactly which chains, addresses and topics a module will be given without building the guest. Any design that moves subscriptions into guest code has to keep that property, whether by emitting a manifest the operator still reviews or by keeping host-side bounds on what a module may subscribe to. A subscription that is only visible by disassembling a wasm is a regression in auditability even if it is nicer to write.That argues for the first option now, and the second only if the emitted artefact stays reviewable.
Timing
Cheaper before the M5 carve. It touches
nexum-module-macrosandnexum-runtime(L1),videre-macros(L2) and the modules (L3), which become three repositories afterwards.Acceptance criteria
A drift between a module's declared topic and the topic the host filters on is caught mechanically, not by a per-module hand-written test.
ethflow-watcheris covered by whatever mechanism lands, since it is currently unguarded.The operator can still determine a module's subscriptions without building it.