How I Found a Validator Slashing Oracle in a Blockchain Remote Signer
A consensus-signing service exposed an unauthenticated gRPC endpoint that would sign arbitrary bytes without message semantics or double-sign protection. Reproducing it required building the exact 75-byte canonical vote encoding from the project's own codec before anything could be proven.
How I Found a Validator Slashing Oracle in a Blockchain Remote Signer
The target
A public bug bounty program covers three consensus repositories for a proof-of-stake network: the node implementation, the BFT consensus engine, and a remote signing service. The signer is a Go service that holds validator keys and signs consensus messages on request.
The signing service exposes a gRPC API on port 10340. Its default configuration binds to 0.0.0.0, TLS is disabled by default, and the protocol definition carries a single field for the message payload:
service Signer {
rpc Sign(SignRequest) returns (SignResponse);
}
message SignRequest {
bytes message = 1;
}
No caller identity. No authentication. No message-type filter.
The bug class
A remote signer exists to enforce one invariant above all: the same key must never sign two conflicting messages at the same height and round. In BFT consensus, that is the equivocation condition. Evidence of two conflicting signed votes is enough to destroy the validator's staked collateral automatically.
The original upstream project this service descends from passes signing requests over a UNIX domain socket from the node process on the same machine, and it keeps anti-double-sign state inside the signer. The fork moved the API to network gRPC and dropped that state layer entirely. The docs describe port 10340 as the external API port.
So the attack is: anyone who can reach the port can submit two conflicting consensus votes and receive two valid signatures. The network's own evidence module classifies the pair as a slashable offence. The signing oracle is the missing guardrail.
The part that took eleven iterations
Proving this required getting the signed payload byte-exact. Consensus messages are SSZ-encoded, and the first version of the proof used a guessed 66-byte layout. When the encoding was checked against the project's own codec, it failed to decode entirely: the real canonical form is 75 bytes, the round is an Option with a one-byte selector, and the offsets are variable-length.
The working approach was to stop guessing and generate the payload with the target's own Rust crates: pin the consensus engine to the same tag the node uses, write a small harness that constructs two conflicting votes at the same height and round but for different values, and call to_sign_bytes() on each. The codec produces the canonical encoding; the harness proves both decode back into valid votes, which is the exact definition of the equivocation the evidence module checks for.
Both votes were then signed by the unauthenticated service in under 6 milliseconds total, and both signatures verified against the validator's public key, which the same service also hands out to anyone who asks.
Impact and honesty
An unauthenticated network attacker who can reach the signer port can force a validator into a slashable double-vote state. Severity was argued as Critical (CVSS 9.1, integrity and availability of consensus at stake, no privilege required), with the honest caveat that a triager could downgrade it if the deployment in question binds the port to localhost only. No key material is ever exposed, and nothing beyond the signed consensus messages leaves the box.
The report was eventually closed as a duplicate of an earlier submission, ten days prior. That is the game on mature programs: the obvious surface on headline assets gets picked fast. What carried over from the engagement was the harness, the canonical encoding generator, and the reproduction environment, which turned out to be reusable ammunition for the next target on the same scope.
Takeaways
- A remote signer without watermarking or double-sign state is a slashing oracle, whatever the transport.
- Never hand-assemble consensus payloads: generate them from the target's own codec and prove the roundtrip before writing a report.
- On programs with more than 500 resolved reports, assume the headline findings are taken and go deep-first: compile the crates, write the differential tests, bring receipts.