Skip to content

fix(error): ControlError.data is required, so a bare JSON-RPC -32601 never decodes — every consumer misreads version skew as a transport failure #29

Description

@MichaelTaylor3d

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)

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