Documentation
¶
Overview ¶
Package audit lets moderators spot-check each other. It is wired: the moderator answers challenges on ChallengeRoute, spot-checks a random peer every few minutes, and files every decided round as reference material. One thing is deliberately NOT wired — no verdict, vote or connection is refused because of a Ledger standing. See "What is missing" below for why that step needs more than this package can prove on its own.
The problem it targets: the vote round (see the moderator package) stops a forged verdict, but a moderator that actually joins the network and votes dishonestly — or a bot that "votes" without running any model at all — is only outvoted, never identified. Audit closes that: any moderator may pick a random peer and hand it a spot-check ("challenge"): moderate exactly this text. The signed answer is compared against the probe's expected class and accumulated per peer in a Ledger; a peer whose answers are systematically wrong, absent or cryptographically invalid loses standing down to Banned.
Three constraints shape everything here:
Challenges must not be predictable. An earlier draft shipped a list of invented probe texts, which is worthless: the source is public, so anyone can tabulate "this text means unsafe" and answer correctly with no model at all. Challenges are therefore drawn from Corpus — real texts a vote round already ruled on, with the quorum's verdict as ground truth. That pool is unique to each node, changes as traffic does, and costs nothing to build, since the round already fetched the text and already agreed on the answer.
Verdict CLASSES are compared, never bytes or scores, and only statistically: a single disagreement is noise, a systematic pattern is a fake. The thresholds in ledger.go encode that tolerance. The current tolerance assumes every moderator runs the same model — see "What is missing" below.
Cross-platform: no float comparisons, no runtime assumptions. The challenge binds to hex(sha256) over the raw UTF-8 bytes of the text, which is identical on every architecture, and the response is a signed JSON event like every other Warpnet wire message.
What is missing for a sound fake-moderator test ¶
Under the same-model assumption an audit catches a peer with no model at all (constant answers, coin flips, silence) and a peer answering under someone else's identity. That is worth having, and it is all this package claims. Everything below stands between that and a verdict trustworthy enough to disqualify anyone — which is exactly why no vote, verdict or connection is currently refused on a Ledger standing.
A single auditor must not judge. A standing here is one node's opinion, formed from references only it has seen. Acting on it lets a malicious auditor disqualify honest moderators — a worse attack than the one being defended against. A ban has to come from independent auditors agreeing, which means gossiping the signed challenge/response transcripts and deciding by quorum, the same shape the vote round already uses for content.
Model identity has to be established, not assumed. The moment moderators legitimately differ (a newer Llama Guard, another guard model, a different quantization), a class disagreement no longer separates a liar from an honest peer. Options, roughly by cost: pin a model version per network epoch and make the version part of the protocol; or have peers attest what they run and calibrate the tolerated disagreement per model pair from observed rates; or restrict challenges to references the network agreed on unanimously, where model differences matter least.
References must be beyond dispute. Corpus trusts the quorum, so a round decided by a colluding majority poisons every reference it produces. Weighting references by how lopsided the round was, and dropping those carried by a bare majority, limits the damage without removing it.
A correct answer is not proof of work done. A peer can relay the text to a third party, or recognise a reference it already holds in its own corpus. Nothing here separates "runs a model" from "can obtain an answer"; only economic or latency-bound arguments do, and both are weak.
Identity still costs nothing. Audit measures behaviour per key, so a banned peer returns under a fresh one. Standing starts at Probation for that reason, but a probation period raises the price of a new identity without setting one. Absent a real cost (stake, proof of work, a vouching web), a patient attacker outlasts any behavioural test.
Until (1) and (2) hold, the honest reading of a Banned standing is "I should not rely on this node", not "this node is a fake".
Index ¶
- Constants
- func ContentHash(text string) string
- func StreamChallengeHandler(engine Engine, sign ResponseSigner) warpnet.WarpHandlerFunc
- type Auditor
- type Challenge
- type ChallengeResponse
- type Challenger
- type Corpus
- type Engine
- type Ledger
- type Outcome
- type PeerReport
- type ResponseSigner
- type Result
- type Standing
- type StreamChallenger
- type Streamer
Constants ¶
const ( ErrEmptyChallenge warpnet.WarpError = "audit: empty challenge text" ErrChallengeHashMismatch warpnet.WarpError = "audit: challenge text does not match its content hash" )
const ChallengeRoute = "/public/get/moderate/challenge/0.0.0"
ChallengeRoute is the moderator-to-moderator audit spot-check route. Reserved for the audit protocol; no handler is registered for it yet.
const ErrChallengeRefused warpnet.WarpError = "audit: peer refused the challenge"
const ErrResponseSignature warpnet.WarpError = "audit: challenge response signature invalid"
Variables ¶
This section is empty.
Functions ¶
func ContentHash ¶
ContentHash is the cross-platform binding between a challenge and its response: hex(sha256) over the raw UTF-8 bytes of the text. Identical on every architecture, no normalization games.
func StreamChallengeHandler ¶
func StreamChallengeHandler(engine Engine, sign ResponseSigner) warpnet.WarpHandlerFunc
StreamChallengeHandler answers audit spot-checks: run the local engine on exactly the challenged text and return a signed verdict. NOT REGISTERED anywhere yet — wiring it under ChallengeRoute on the moderator node is the integration step.
Types ¶
type Auditor ¶
type Auditor struct {
// contains filtered or unexported fields
}
Auditor drives moderator-to-moderator spot-checks: pick a random peer, hand it a probe, judge the signed answer, feed the ledger.
func NewAuditor ¶
func (*Auditor) ChallengeRandomPeer ¶
ChallengeRandomPeer picks one eligible peer at random and runs a single spot-check against it. A nil Result means there was nothing to run: no eligible peer (empty set, all on cooldown, or only self), or a corpus too thin to judge anyone by. An audit never fails: a peer that cannot answer, or answers garbage, has simply earned that outcome.
type Challenge ¶
type Challenge struct {
ChallengeID string `json:"challenge_id"`
Text string `json:"text"`
ContentHash string `json:"content_hash"`
TimeAt time.Time `json:"time_at,omitzero"`
}
Challenge is a spot-check one moderator sends to another: "moderate exactly this text". ContentHash is hex(sha256) over the raw UTF-8 bytes of Text, so the response binds to the same content on any platform — no float math, no runtime assumptions.
NOT WIRED YET: no handler is registered for the challenge route; this is the wire shape for the future moderator-audit protocol. It lives in this package rather than the shared event package because only moderators exchange challenges — no member node ever sends or receives one.
func BuildChallenge ¶
BuildChallenge draws a reference from the corpus and wraps it as a spot-check. The bool is false while the corpus is too thin to audit anyone with; expectUnsafe is the verdict a round already reached on that text, which is what the answer gets compared against.
type ChallengeResponse ¶
type ChallengeResponse struct {
ChallengeID string `json:"challenge_id"`
ContentHash string `json:"content_hash"`
Result domain.ModerationResult `json:"result"`
Reason *string `json:"reason,omitempty"`
Model domain.ModelType `json:"model"`
ModeratorID domain.ID `json:"moderator_id"`
TimeAt time.Time `json:"time_at,omitzero"`
// Signature is base64(ed25519) over SigningBytes with the responder's
// node key; it makes the answer non-repudiable audit evidence.
Signature string `json:"signature,omitempty"`
}
ChallengeResponse is the challenged moderator's signed verdict on the probe text. Moderators are free to run different models (Model reports which one answered), so auditors must compare verdict classes statistically, never byte-for-byte.
func (ChallengeResponse) Signed ¶
func (e ChallengeResponse) Signed(privKey ed25519.PrivateKey, moderator domain.ID, model domain.ModelType) ChallengeResponse
Signed returns a stamped and signed copy of the response, answering as moderator with the model that actually judged the text.
func (ChallengeResponse) VerifiedFrom ¶
func (e ChallengeResponse) VerifiedFrom(peer string) error
VerifiedFrom checks that the response really came from peer: the signature must verify against the public key carried by that peer id, so a valid answer signed by anyone else counts for nothing.
type Challenger ¶
type Challenger interface {
Ask(peer string, ch Challenge) (ChallengeResponse, error)
}
Challenger delivers a challenge to a peer and brings its answer back. Everything about how that happens — streams, routes, encoding — lives behind this interface, so the audit logic never has to know.
type Corpus ¶
type Corpus struct {
// contains filtered or unexported fields
}
Corpus is the reference material an audit draws on: real texts a vote round has already ruled on, with the quorum's verdict as ground truth.
It replaces the synthetic probe list an earlier draft carried. A fixed list of made-up texts is worthless as a challenge: the code is public, so anyone can tabulate "this template means unsafe" and answer correctly with no model at all. References taken from live traffic cannot be tabulated ahead of time — the auditor picks from an ever-changing pool only it has seen — and they cost nothing to produce, since the round already fetched the text and already agreed on the verdict.
Keeping the two classes in separate rings matters: a challenger draws them in equal measure, so a peer that answers a constant class scores about half and lands under the ban threshold either way.
func (*Corpus) Remember ¶
func (c *Corpus) Remember(text string, verdict domain.ModerationResult)
Remember files a text a round has ruled on. Call it when a round decides, with the text that was judged and the quorum's verdict: the agreement of several independent moderators is a stronger reference than any single node's opinion, which is exactly what an audit needs.
Texts already in the corpus are not re-filed, so a report storm over one tweet cannot flood the references with copies of it.
func (*Corpus) Sample ¶
Sample draws one reference, alternating classes at random so neither is predictable. The bool is false while either class is still empty: until this node has seen both a clean and a moderated text there is nothing to audit anyone against, and guessing would be worse than waiting.
type Engine ¶
Engine is the moderation engine slice the respondent side needs — the same shape the moderator package uses.
type Ledger ¶
type Ledger struct {
// contains filtered or unexported fields
}
Ledger accumulates audit outcomes per moderator peer. In-memory and local-first by design: every node judges from its own evidence; sharing signed transcripts across nodes is a later layer.
func NewLedger ¶
func NewLedger(events warpnet.PeerEmitter) *Ledger
NewLedger reports what it concludes into events, the fan-out of the moderator that owns it.
func (*Ledger) Snapshot ¶
func (l *Ledger) Snapshot() map[string]PeerReport
func (*Ledger) StandingOf ¶
StandingOf reports the local trust state; peers never audited are on probation, not trusted.
type Outcome ¶
type Outcome int
Outcome is the auditor's classification of one challenge exchange.
const ( // OutcomeCorrect: answered with the expected verdict class. OutcomeCorrect Outcome = iota // OutcomeWrong: answered, cryptographically valid, but with the wrong // class on a flagrant probe. One of these is model noise; a pattern is // a fake — the thresholds below draw that line. OutcomeWrong // OutcomeUnreachable: no usable answer (transport error, timeout, or // an error reply). Could be the network's fault, so it degrades // standing only in bulk and never to Banned. OutcomeUnreachable // OutcomeInvalid: an answer that is wrong cryptographically or // protocol-wise — bad signature, foreign responder id, mismatched // challenge binding. Never accidental; two of these ban the peer. OutcomeInvalid )
type PeerReport ¶
PeerReport is a read-only snapshot row, the shape a future reputation gossip or admin surface would consume.
type ResponseSigner ¶
type ResponseSigner func(ChallengeResponse) ChallengeResponse
ResponseSigner stamps identity, model and signature onto an outgoing challenge response.
func NewResponseSigner ¶
func NewResponseSigner(privKey ed25519.PrivateKey, selfID domain.ID, model domain.ModelType) ResponseSigner
NewResponseSigner builds the production signer: the responder's node key and the model it actually runs (self-reported — audits judge classes, not model claims).
type Result ¶
type Result struct {
Peer string
Outcome Outcome
Expected bool // expectUnsafe: true means the honest class is FAIL
Response ChallengeResponse
}
Result is one finished challenge exchange, kept alongside the raw signed response — the transcript a future reputation gossip can re-verify.
type Standing ¶
type Standing int
Standing is the local trust state derived from a peer's audit history.
type StreamChallenger ¶
type StreamChallenger struct {
// contains filtered or unexported fields
}
StreamChallenger is the wire side of an audit: it carries a challenge over a Warpnet stream and decodes the answer. It is the only place in this package that knows about routes, streams or encoding — the audit logic sees nothing but Challenger.
func NewStreamChallenger ¶
func NewStreamChallenger(node Streamer) *StreamChallenger
func (*StreamChallenger) Ask ¶
func (c *StreamChallenger) Ask(peer string, ch Challenge) (ChallengeResponse, error)