Task
ControlError.data is required with no serde default, so a bare JSON-RPC error never decodes — and
every consumer misreads it as a transport failure.
src/error.rs:253 pub data: ControlErrorData, ← no Option, no #[serde(default)]
Why that is a live defect rather than a strictness preference
A real dig-node answers an unresolved control.* method with no data field at all:
{"jsonrpc":"2.0","id":id,"error":{"code":-32601,"message":"method not found"}}
Emitted at dig-node/crates/dig-node-core/src/seams/dig_rpc/dispatch.rs:790-791, returned verbatim via
dig-node-service/src/server.rs:1169-1170.
-32601 is the JSON-RPC 2.0 standard method-not-found code, not a DIG-specific one, so it will never
carry a DIG data payload. A required data therefore makes the single most predictable error in the
protocol undecodable.
The measured consequence — this already caused a permanent spend lockout
In dig-app, the response failed to decode, so METHOD_NOT_FOUND never reached the Unsupported arm,
degrade() never fired, held() returned Unavailable, and select_and_reserve propagated it:
every send, CAT send and mint refused, permanently, against any node not yet serving the reservation
methods.
Proven through the real transport with a control differing in exactly one field:
PROBE_A_CONTROL_ACTUAL = Err(Unsupported) ... ok
PROBE_A_ACTUAL = Err(Unavailable("...missing field `data` at line 1 column 76")) ... FAILED
dig-app fixed it at its own shared decode path (crates/dig-app-core/src/control.rs — strict decode
first, lenient retry second, symbol recovered via ControlErrorCode::from_code). But that is one
consumer working around a contract-level defect, and the next consumer will rediscover it the same
expensive way.
The failure mode is the worst kind: a version-skew error becomes a transport error. A caller that
should learn "this node does not support that method yet, degrade gracefully" instead learns "the
network is broken" — and takes the wrong recovery.
Scope
Default the field, so a bare error decodes and callers branch on the JSON-RPC code:
#[serde(default)]
pub data: ControlErrorData,
— or make it Option<ControlErrorData>, which is more honest about the wire but is a breaking change
for every consumer that reads .data unconditionally. Decide and say which, with the reasoning;
#[serde(default)] is additive if ControlErrorData can carry a sensible Default.
Say normatively in SPEC.md what a caller may assume: data is present for DIG-catalogued codes and
absent for standard JSON-RPC ones. That distinction is the actual contract and it is currently unstated.
Evidence
- Reproduce first: the exact dig-node bytes above must currently fail to decode. Red for the right
reason — assert on the missing-field error, not merely that decoding failed.
- Prove the fix is specific, not blanket. A lenient decode that also swallows genuinely malformed
responses trades one defect for silent corruption. Keep a control: a well-formed error carrying
data must still decode with its symbol intact, and a malformed one must still fail.
- Both directions: a bare
-32601 decodes and yields MethodNotFound; a catalogued -32046 decodes
and yields WalletCoinsReserved.
- Read passed-counts, never
ok. Any mutation asserts its patch applied; parse the test result:
line, never stderr. One cargo process at a time.
Provenance
Found by the pre-merge security audit of DIG-Network/dig-app PR #266 (eco#3127), where it presented as
a permanent spend lockout on the default path, on every machine. Verified independently against
origin/main before filing.
Related: dig-node-control-interface#26 (cliff.toml renders breaking releases as ordinary features) ·
dig-node-control-interface#27 (the -3204x band allocation)
Task
ControlError.datais required with no serde default, so a bare JSON-RPC error never decodes — andevery consumer misreads it as a transport failure.
Why that is a live defect rather than a strictness preference
A real dig-node answers an unresolved
control.*method with nodatafield at all:{"jsonrpc":"2.0","id":id,"error":{"code":-32601,"message":"method not found"}}Emitted at
dig-node/crates/dig-node-core/src/seams/dig_rpc/dispatch.rs:790-791, returned verbatim viadig-node-service/src/server.rs:1169-1170.-32601is the JSON-RPC 2.0 standard method-not-found code, not a DIG-specific one, so it will nevercarry a DIG
datapayload. A requireddatatherefore makes the single most predictable error in theprotocol undecodable.
The measured consequence — this already caused a permanent spend lockout
In
dig-app, the response failed to decode, soMETHOD_NOT_FOUNDnever reached theUnsupportedarm,degrade()never fired,held()returnedUnavailable, andselect_and_reservepropagated it:every send, CAT send and mint refused, permanently, against any node not yet serving the reservation
methods.
Proven through the real transport with a control differing in exactly one field:
dig-app fixed it at its own shared decode path (
crates/dig-app-core/src/control.rs— strict decodefirst, lenient retry second, symbol recovered via
ControlErrorCode::from_code). But that is oneconsumer working around a contract-level defect, and the next consumer will rediscover it the same
expensive way.
The failure mode is the worst kind: a version-skew error becomes a transport error. A caller that
should learn "this node does not support that method yet, degrade gracefully" instead learns "the
network is broken" — and takes the wrong recovery.
Scope
Default the field, so a bare error decodes and callers branch on the JSON-RPC
code:— or make it
Option<ControlErrorData>, which is more honest about the wire but is a breaking changefor every consumer that reads
.dataunconditionally. Decide and say which, with the reasoning;#[serde(default)]is additive ifControlErrorDatacan carry a sensibleDefault.Say normatively in
SPEC.mdwhat a caller may assume:datais present for DIG-catalogued codes andabsent for standard JSON-RPC ones. That distinction is the actual contract and it is currently unstated.
Evidence
reason — assert on the missing-field error, not merely that decoding failed.
responses trades one defect for silent corruption. Keep a control: a well-formed error carrying
datamust still decode with its symbol intact, and a malformed one must still fail.-32601decodes and yieldsMethodNotFound; a catalogued-32046decodesand yields
WalletCoinsReserved.ok. Any mutation asserts its patch applied; parse thetest result:line, never stderr. One cargo process at a time.
Provenance
Found by the pre-merge security audit of
DIG-Network/dig-appPR #266 (eco#3127), where it presented asa permanent spend lockout on the default path, on every machine. Verified independently against
origin/mainbefore filing.Related: dig-node-control-interface#26 (cliff.toml renders breaking releases as ordinary features) ·
dig-node-control-interface#27 (the
-3204xband allocation)