feat(#163 CC-13 inc-2-live): real gossip BLS quorum co-signer (default-off)#179
Conversation
…e stub) SP completed the on-chain prereqs (applyBLSAggregator + addValidator×3 + registerBLSPublicKey×3, slots dvt1=1/dvt2=2/dvt3=3). This replaces the PendingSlotCoSigner stub with the real gossip-based BLS quorum co-signer that drives on-chain GToken slashing. - slash-consensus.ts: widen the seam to a structured CoSignRequest (so peers verify from first principles, never trust a raw hash); pure buildSignerMask (slot s → bit s-1, PINNED against BLSAggregator.sol:772 — [1,2,3]=7n) + recomputeMessageHash; MAX_VALIDATORS=13; QUORUM_COSIGNER DI token. PendingSlotCoSigner still throws. - gossip-quorum-cosigner.ts (NEW): responder verifyAndSign (fail-closed on not-armed / not-watchlisted / hash-mismatch / independent-violation-unconfirmed / execute proofHash≠evidenceHash / no-slot) + requester coSign (self-contribute, per-response cryptographic BLS verify + on-chain slot→key binding, STRICT threshold → throw on under-threshold, never submit a reverting proof). Aggregate via aggregateSignaturesOnly→encodeToEIP2537. - gossip: cosign-request/response message types (point-to-point ttl=0, requestId correlation, fresh messageId), registerCoSignHandler + requestCoSignatures collector. Transport only — zero slash logic in GossipService. - blockchain.service.ts: getValidatorAtSlot / getBlsPublicKeyAtSlot / getSlotForValidator (on-chain authoritative slot source). - audit.service.ts: call sites pass CoSignRequest; verifyViolationForCoSign responder verifier + shared isCreditOverLimit; arm co-signer at bootstrap. - audit.module.ts: factory provider (armed→GossipQuorumCoSigner, disarmed→stub). - configuration.ts: auditCoSignTimeoutMs(15s) / auditSlashThresholds(2/3/3) / auditMaxSlots(13) / auditSlotMap. Still default-off (AUDIT_EXECUTE_SLASH). All inc-2 invariants preserved. 291/291 tests (+28), type-check/build/lint clean. LIVE-ONLY (needs dvt1/2/3 deployed): real signerMask/sigG2 accepted by verifyAndExecute pairing + real 3-node gossip agreement. Claude-Session: https://claude.ai/code/session_01JrdmiUk4A258FmhDSKn9aj
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…Low) Codex found no Critical (the core 'a lying node cannot force an unjust slash' holds), but 6 hardening issues blocking live-arming. All fixed: - HIGH: clamp AUDIT_SLASH_THRESHOLDS UP to a pinned floor (WARNING>=2, MINOR>=3, MAJOR>=3) so a misconfig can't let one signature pass as quorum. - HIGH: static-call preflight before queueSlashWithProof/executeSlashWithProof — a staticCall revert throws (→ degrade to file+archive) BEFORE spending gas, so any signerMask/sigG2/DST/threshold/slot mismatch is caught free instead of a paid revert. - MED: gossip requestCoSignatures now counts only VALIDATED responses (validate+dedupKey callbacks) toward threshold — a bad peer can't crowd out honest signers (liveness DoS). - MED: queue co-sign now also requires evidenceHash present AND == re-derived proofHash (was execute-only) — no bogus-evidence queue quorum. - MED: ProposalCreated parse requires log.address==dvtValidator AND operator/level match. - LOW: widen ProofIdentity (availableCredit, slashLevel, violationBlockHash, registry/ superPaymaster/dvtValidator/apntsToken) so proofHash is fully content-addressed; add getBlockHash; responder re-reads epoch blockHash fail-closed. Still default-off. All inc-2/inc-2-live invariants preserved. 313/313 tests (+21), type-check/build/lint clean. Claude-Session: https://claude.ai/code/session_01JrdmiUk4A258FmhDSKn9aj
…+ strict threshold parse Codex R2: no Critical/High (all R1 blockers resolved). Two live-arming liveness residuals (both already fail-closed), fixed: - MED: bound co-sign response validation work. A compromised peer that knows the requestId could flood responses, each hitting async validate (BLS verify + on-chain reads) before dedup. Now: EXACT resends (signer|slot|sig) are dropped BEFORE validate, and total validations per request are capped (maxValidations, default 64). Keyed on the FULL response — never on slot alone — so an early bogus slot claim can NOT poison the honest signer's real response for that slot (post-validate slot dedup still decides quorum). +2 tests (exact-dedup+cap, no-poisoning). - LOW: strict AUDIT_SLASH_THRESHOLDS numeric parse — '4oops' was read as 4 by parseInt; now ^[0-9]+$ + Number.isSafeInteger, malformed → ignored → safe floor kept. +2 tests. Still default-off. 317/317 tests (+4). type-check/build/lint clean. Codex verdict: default-off merge sound; these were the last live-arming liveness gaps. Claude-Session: https://claude.ai/code/session_01JrdmiUk4A258FmhDSKn9aj
clestons
left a comment
There was a problem hiding this comment.
PK Review — YetAnotherAA-Validator#179 (gossip BLS quorum co-signer)
Verdict: REQUEST CHANGES — 2 confirmed findings + 1 PK-added liveness concern. Safety design is solid (fail-closed at every gate, BLS crypto verified end-to-end, threshold strictly enforced). The issues below are pre-production blockers before AUDIT_EXECUTE_SLASH=true is enabled on live DVT nodes.
[Confirmed] Medium — resolveOwnSlot O(maxSlots) sequential RPC on every call
File: src/modules/audit/gossip-quorum-cosigner.ts:1205
Every coSign AND every verifyAndSign (which is also called inside coSign for self-contribution) runs a serial loop:
for (let slot = 1; slot <= this.maxSlots; slot++) {
const onchain = await this.blockchain.getBlsPublicKeyAtSlot(this.blsAggregatorAddress, slot);
...
}With AUDIT_MAX_SLOTS=13 (default) and RPC latency of 200–500ms per call, this scan alone consumes 2–6s of the 15s co-sign timeout window — before a single peer response has been requested. Worse, it runs twice in one coSign call (once as self-contribution inside verifyAndSign, once at the start of coSign).
Fix: Cache own slot after first successful resolution. The slot only changes if the operator re-registers a BLS key (a rare, deliberate action), so a process-lifetime cache with a stale flag is sufficient:
private ownSlotCache: number | null | undefined = undefined;
private async resolveOwnSlot(compressedPubKey: string): Promise<number | null> {
if (this.ownSlotCache !== undefined) return this.ownSlotCache;
// ... existing scan logic ...
this.ownSlotCache = result;
return result;
}[PK-added] Medium — Validation budget exhaustion by compromised peer
File: src/modules/gossip/gossip.service.ts:690 (added in this PR)
The handleCoSignResponse path deduplicates by signerNodeId|slot|signatureCompact and caps total validation work at maxValidations=64. A compromised connected peer who knows the requestId (gossip-broadcast) can inject 64 responses with distinct invalid signatureCompact values — each passes seenExact (different sig), each increments validationsStarted. After 64 entries, line 690 silently drops ALL subsequent responses, including legitimate ones from honest validators.
Impact: Liveness — the timeout expires, coSign throws "only N valid unique-slot signatures", the slash is deferred. Safety is preserved (no wrong slash). With N=3 DVT nodes, a single compromised peer can reliably block all slashing operations.
Fix: Rate-limit per signerNodeId instead of (or in addition to) global budget, or reduce maxValidations to maxSlots * 2 (26 for N=13) so honest nodes (≤13 slots × 1–2 retries) comfortably fit while a budget-exhaustion attack requires a much tighter injection window.
[Confirmed] Low — Double RPC per peer response in validateResponse
File: src/modules/audit/gossip-quorum-cosigner.ts:1181–1192
validateResponse calls both getBlsPublicKeyAtSlot AND getValidatorAtSlot for each peer response. A non-null BLS public key already implies a live validator at that slot — getValidatorAtSlot is redundant and doubles RPC cost on the already-tight validation path.
Fix: Remove the getValidatorAtSlot call; rely solely on getBlsPublicKeyAtSlot returning non-null as proof of slot occupancy.
Rejected
- F4 (parseSlashThresholds no floor-clamping): Fixed in commit 2 via
SLASH_THRESHOLD_FLOOR. Final state correctly preventsMINOR:1from passing. No action needed. - F2 (arm() double registration):
registerCoSignHandleris single-field assignment (this.coSignHandler = fn) — repeatedarm()calls overwrite, not accumulate. Finding was incorrect.
PK Summary
| Round | Challenger | Result |
|---|---|---|
| 1 | Codex | F1 CONFIRM · F2 CHALLENGE (correct) · F3 CONFIRM · F4 CONFIRM · MISSED: budget exhaustion |
Two-round cap not needed — no critical misses requiring second round.
Self-assessment: Security design is strong — the structured CoSignRequest, independent violation re-confirmation, slot↔key on-chain binding, and STRICT threshold enforcement are all correct. The three findings are performance/liveness concerns on the hot path, not safety holes. Fix F1 (cache) and address the budget exhaustion before enabling on live DVT nodes.
… + per-conn rate limit) Copilot REQUEST CHANGES on #179 — 3 liveness/perf issues (no safety gate touched): - own-slot cache: resolveOwnSlot memoizes the node's stable on-chain slot (was up to 13 serial getValidatorAtSlot RPCs per co-sign); null (unregistered) is NOT cached so a later registration is picked up; refreshOwnSlot() forces re-scan. getSlotForValidator now scans slots in parallel (Promise.all), lowest match. - validateResponse: drop the redundant getValidatorAtSlot (getBlsPublicKeyAtSlot non-null already implies non-zero + active validator); add a per-coSign keyCache shared by the collector validate callback + the post-collection loop so same-slot reads hit RPC once. - per-connection rate limit: the global maxValidations=64 could be exhausted by ONE compromised peer flooding distinct bogus responses (signerNodeId is forgeable). Now rate- limit by the CONNECTION (ws, the un-forgeable identity): each connection gets at most perConnCap (default 4) validations, so a flooder is bounded and honest signers on other connections still reach quorum. Exact-dedup + global cap retained as backstops. Still default-off. Safety gates unchanged (Codex-confirmed sound). 331/331 tests, type-check/build/lint clean. Claude-Session: https://claude.ai/code/session_01JrdmiUk4A258FmhDSKn9aj
clestons
left a comment
There was a problem hiding this comment.
PK Review (incremental) — YetAnotherAA-Validator#179 v2
Verdict: REQUEST CHANGES — Previous 3 findings fully addressed. 3 new issues found in the incremental commits, one MEDIUM (coordinated-upgrade constraint) requiring documentation.
Previous findings — all fixed ✓
| Finding | Status |
|---|---|
F1 — resolveOwnSlot O(maxSlots) serial RPC, no cache |
Fixed: ownSlot/ownSlotResolved permanent cache; null NOT cached; refreshOwnSlot() for forced re-scan |
F3 — validateResponse double RPC (getValidatorAtSlot redundant) |
Fixed: getValidatorAtSlot removed; non-null getBlsPublicKeyAtSlot already implies active validator; per-call keyCache added |
PK-added — maxValidations=64 exhaustible by one peer (distinct-sig flood, liveness) |
Fixed: per-connection cap perConnCap=4 (WebSocket identity, un-forgeable); flooder on one connection capped before honest peers on other connections are crowded out |
Bonus security hardening verified:
- HIGH 1: slash threshold floor clamp (
SLASH_THRESHOLD_FLOOR = 2/3/3) —MINOR:1misconfiguration can no longer bypass 3-of-3 quorum - HIGH 2: static-call preflight on
queueSlashWithProof/executeWithProof— deterministic reverts surface before gas is spent - MEDIUM 1: collector now counts only validated + deduped-by-slot responses toward threshold (bogus responses cannot crowd out honest signers or force premature resolution)
- MEDIUM 2:
evidenceHashbinding extended to queue step (was execute-only) — malicious requester cannot form a real queue quorum while attaching bogus evidence - MEDIUM 3:
ProposalCreatedlog gated to the calling DVTValidator address + matching operator/level — wrong-contract log cannot steal another proposal's id
[New] Medium — Coordinated upgrade required (ProofIdentity schema break)
Files: src/modules/audit/proof-archive.ts, audit.service.ts, gossip-quorum-cosigner.ts
ProofIdentity expanded from ~4 fields to 9 (availableCredit, violationBlockHash, slashLevel, registry, superPaymaster, dvtValidator, apntsToken added). The CoSignRequest type carries no schema version field.
Consequence: a node running old code re-derives the old-schema proofHash; a proposer on new code sends the new-schema evidenceHash → permanent mismatch → co-sign refused. All three DVT nodes must upgrade simultaneously, or co-signing is broken during the window when nodes run mixed versions.
Suggested fix: Document in DEPLOYMENT.md or the upgrade runbook:
⚠️ This commit changesProofIdentity's hash input schema. Upgrade all DVT nodes simultaneously; do NOT rolling-upgrade. A split-version cluster will refuse all co-sign requests until all nodes are on the new schema.
Optionally add a schemaVersion: 2 field to CoSignRequest so responders can detect and reject stale requests with an explicit error rather than a silent mismatch.
[New] Low — getBlockHash on non-archive node → permanent liveness failure
File: src/modules/blockchain/blockchain.service.ts:971–980, audit.service.ts:663
verifyEvidence now calls getBlockHash(blockTag) and fails closed (return NO) if the result is null. getBlockHash returns null on any provider exception OR if getBlock(number) returns null (block pruned).
Non-archive RPC nodes (default geth/reth retain ~128 blocks). If a DVT node's provider is non-archive, any violation whose epoch block is older than ~128 blocks will have its block hash unresolvable → verifyEvidence returns NO → legitimate slash cannot co-sign.
Suggested fix: Require archive-capable RPC for DVT nodes and document this in the deployment guide. Optionally emit a warning log on null to distinguish "block pruned" from "transient RPC failure":
if (!violationBlockHash) {
this.logger.warn(`getBlockHash(${blockTag}) returned null — RPC may not be archive-capable`);
return NO;
}[New] Low — getSlotForValidator no early exit (always maxSlots RPCs)
File: src/modules/blockchain/blockchain.service.ts:994–1002
Previous serial for-loop stopped at the first matching slot (O(slot_index) expected). The new Promise.all scan always issues maxSlots (13) concurrent RPC calls — even when the match is at slot 1. The result is correct; this is a mild efficiency regression (13 parallel reads vs average ~2–3 serial reads when most operators are near the front of the list).
Acceptable trade-off for the wall-clock speedup, but worth noting: with 3 registered nodes each making this call on co-sign events, it's 39 RPCs vs ~9 before.
PK Summary
| Finding | PK Verdict |
|---|---|
| F-new-1 — ProofIdentity schema break, coordinated upgrade required | [CONFIRM] Codex |
| F-new-2 — getBlockHash prune → permanent liveness failure | [CONFIRM] Codex |
| F-new-3 — getSlotForValidator full-scan (no early exit) | [CONFIRM] Codex |
Self-assessment: Excellent security hardening round. All 3 original findings and 5 additional security issues (HIGH 1/2, MEDIUM 1/2/3) addressed with comprehensive tests. The new MEDIUM is a deployment coordination concern rather than a logic bug — the code is correct, but the upgrade runbook needs an explicit "all-at-once upgrade" warning. F-new-2 and F-new-3 are acceptable tradeoffs for the correctness and liveness improvements they enable.
clestons
left a comment
There was a problem hiding this comment.
PK Review (full re-review) — YetAnotherAA-Validator#179
Verdict: REQUEST CHANGES — Core gossip BLS quorum co-sign architecture is sound and fail-closed. 3 deployment/operational issues remain open; the two HIGH and three MEDIUM security items were correctly addressed across the 4 commits.
Security model — verified correct
The core invariant — "a lying node cannot force an unjust slash" — holds through the PR:
- Responder (
verifyAndSign): 9 fail-closed checks: armed, watchlisted, messageHash recompute (never trusts requester's copy), independent violation re-confirmation at epoch block, evidenceHash == re-derived proofHash for BOTH queue and execute steps, own on-chain slot resolved, BLS sign locally-recomputed hash. - Requester (
coSign): self-contributes through the same responder gate, crypto+on-chain-binds every peer response (validateResponse: sig verify +getBlsPublicKeyAtSlotslot binding), strict per-severity threshold with post-collection dedup by slot, never aggregates under-threshold. - Static-call preflight on
queueSlashWithProof/executeSlashWithProofcatches any signerMask/sigG2/DST/threshold mismatch before spending gas. - ProposalCreated log bound to the calling DVTValidator address + matching operator + level (prevents wrong-proposal-id binding).
- Slash threshold floor (WARNING≥2, MINOR≥3, MAJOR≥3) — misconfiguration can never weaken below the 3-of-3 pinned policy.
- Collector validation (MEDIUM 1): only validated, unique-slot peer responses count toward threshold; per-connection cap (
perConnCap=4) plus globalmaxValidationsprevent one flooding peer from exhausting the budget before honest signers arrive. resolveOwnSlotcache (finding-1): permanent positive cache, null not cached,refreshOwnSlot()available.validateResponsekeyCache (finding-2): one RPC per unique slot per coSign call, shared between collector and post-collection loop.
[Open] Medium — Coordinated upgrade required: ProofIdentity schema break
Files: src/modules/audit/proof-archive.ts:154–178, audit.service.ts, gossip-quorum-cosigner.ts:150
ProofIdentity expanded from 4 to 9 fields (availableCredit, violationBlockHash, slashLevel, registry, superPaymaster, dvtValidator, apntsToken). CoSignRequest / CoSignRequestPayload carry no schema version field.
A responder running old code re-derives a 4-field proofHash; a proposer running new code sends a 9-field evidenceHash → permanent mismatch → co-sign refused for every violation until ALL nodes are upgraded.
Required fix: Document an explicit "simultaneous upgrade" constraint. Optionally add schemaVersion: 2 to CoSignRequest so responders emit a distinguishable error ("schema mismatch") rather than a silent evidenceHash failure:
// CoSignRequest
schemaVersion?: number; // must equal current PROOF_IDENTITY_SCHEMA_VERSION[Open] Low — getBlockHash on non-archive RPC → permanent liveness failure
Files: src/modules/audit/audit.service.ts:663–668, blockchain.service.ts:493
verifyViolationForCoSign calls getBlockHash(blockTag) and returns NO (fail-closed) on null. getBlockHash returns null on any provider error or pruned block. Non-archive RPC nodes retain only ~128 recent blocks by default (geth/reth/erigon in pruned mode).
If any DVT node's RPC is non-archive and the violation epoch block has been pruned, getBlockHash returns null → that responder permanently refuses the violation — real slashes cannot co-sign even if the violation is genuine.
Suggested fix: Document the archive-node requirement in deployment/DEPLOYMENT.md:
DVT nodes MUST connect to an archive-capable RPC (one that retains full block history). Non-archive providers that prune old blocks will cause
verifyViolationForCoSignto fail-closed for old violations.
Optionally log a diagnostic warning:
if (!violationBlockHash) {
this.logger.warn(`getBlockHash(${blockTag}) null — RPC may not be archive-capable`);
return NO;
}[Open] Low — getSlotForValidator always scans all 13 slots (no early exit)
File: src/modules/blockchain/blockchain.service.ts:711
Changed from serial for-loop (exits at first match, O(avg slot_index)) to Promise.all(1..maxSlots) — always issues 13 concurrent RPC calls even when the match is at slot 1. Functionally correct, but with 3 DVT nodes calling this on each co-sign event, it's 39 RPCs per round-trip instead of ~9.
Acceptable wall-clock trade-off for most deployments; noting it for awareness.
PK Summary
| Finding | PK Verdict |
|---|---|
| F1 — ProofIdentity schema break, coordinated upgrade required | [CONFIRM] Codex |
| F2 — getBlockHash non-archive prune → permanent liveness failure | [CONFIRM] Codex |
| F3 — getSlotForValidator full scan, no early exit | [CONFIRM] Codex |
Self-assessment: Comprehensive security-hardening PR. The slash safety invariant is correctly implemented and adversarially tested with real BLS keys. The 3 remaining issues are operational/deployment concerns; the most actionable is documenting the simultaneous-upgrade requirement and archive-node dependency before live-arming in production.
…rsion + non-archive liveness + O(1) slot) Codex re-review re-confirmed the security model correct (9 fail-closed checks, static-call preflight, collector counts only validated) and 3 open findings. All resolved: - FINDING 3 (perf): own-slot lookup is now O(1) — getRegisteredSlot reads BLSAggregator.getBLSPublicKey(operator).slot directly (one RPC), replacing the 1..13 getValidatorAtSlot scan. Cached; requester still binds every returned slot→key on-chain. - FINDING 2 (non-archive liveness): removed violationBlockHash from the proofHash / ProofIdentity and the responder's getBlockHash(epoch) re-read — reorg-safety already comes from the finality-pinned violationBlock, so a non-archive node no longer permanently fail-closes on an old block header. violationBlockHash stays in the archived evidence for forensics. getBlockHash removed (no remaining caller). - FINDING 1 (rolling-upgrade break): PROOF_SCHEMA_VERSION=2 bound into proofHash + carried in CoSignRequest; verifyAndSign REFUSES with a clear log on version mismatch (explicit + diagnosable, not a silent hash mismatch → lost quorum). docs/INTERFACES.md §3: deploy the DVT fleet atomically, never a rolling partial upgrade. On-chain slash preimages (buildQueue/buildExecuteMessageHash/encodeProof) BYTE-IDENTICAL — ProofIdentity/proofHash is a separate off-chain evidence hash. Still default-off. 345/345 tests, type-check/build/lint clean. Claude-Session: https://claude.ai/code/session_01JrdmiUk4A258FmhDSKn9aj
clestons
left a comment
There was a problem hiding this comment.
PK Review (incremental v4) — YetAnotherAA-Validator#179
Verdict: APPROVE — All 3 previously open findings are fully addressed in this increment. Security model remains fail-closed and correct.
Previously open findings — all fixed ✓
| Finding | Status |
|---|---|
| F-new-1 — ProofIdentity schema break, coordinated upgrade required (MEDIUM) | Fixed: PROOF_SCHEMA_VERSION = 2 added; proofSchemaVersion field in ProofIdentity + CoSignRequest; responder explicitly refuses version-mismatched requests with WARNING log and return null; docs/INTERFACES.md documents atomic upgrade requirement |
F-new-2 — getBlockHash on non-archive RPC → permanent liveness failure (LOW) |
Fixed: violationBlockHash removed from ProofIdentity entirely; getBlockHash() removed from BlockchainService; getBlockHash call removed from verifyViolationForCoSign — reorg-safety now comes only from the finalized epoch block, no historical block header read required; non-archive nodes fully supported |
F-new-3 — resolveOwnSlot O(N) scan (LOW) |
Fixed: resolveOwnSlot() now calls getRegisteredSlot(blsAggregatorAddress, operatorEoa) — one getBLSPublicKey(operatorEoa) RPC call returning (publicKey, slot, isActive) directly; no 1..maxSlots scan; O(1) own-slot resolution |
New increment quality notes
proofSchemaVersionbinding is correct: version field is first inProofIdentity, bound into everycomputeProofHashcall viastableStringify; version changes produce a different hash, making schema drift immediately detectable.violationBlockHashremoval is safe: forensic use retained inEvidenceinterface; reorg-safety argument is sound (finalized epoch block by definition cannot reorg); no test or contract needs a block header hash for proof identity.getRegisteredSlotsecurity model holds: requester'svalidateResponsestill independently binds each responder's returned slot to its on-chain BLS key (getBlsPublicKeyAtSlot), so a misconfigured slot returned byresolveOwnSlotis rejected at validation — safety does not depend on this lookup matching the signing key.- Test coverage: new
proof-archive.spec.tstestsPROOF_SCHEMA_VERSIONbinding; updatedgossip-quorum-cosigner.spec.tscovers version-mismatch refusal and version-match signing; newgetRegisteredSlotdescribe block inblockchain.service.spec.tscovers O(1), null-for-inactive, null-for-slot-0, null-on-revert, null-for-malformed-address.
PK Summary
| Finding | PK Verdict |
|---|---|
| F-new-1 — ProofIdentity schema break (MEDIUM) | [CONFIRM fixed] Codex |
| F-new-2 — getBlockHash non-archive liveness (LOW) | [CONFIRM fixed] Codex |
| F-new-3 — resolveOwnSlot O(N) scan (LOW) | [CONFIRM fixed] Codex |
Self-assessment: This increment cleanly closes all 3 open findings with targeted, well-tested changes. The proof schema versioning mechanism is production-ready and the deployment documentation is updated. APPROVE.
CC-13 inc-2-live — real gossip BLS quorum co-signer
SP completed the on-chain prerequisites (Seeder CC-13):
applyBLSAggregator+addValidator×3+registerBLSPublicKey×3, slots dvt1=1 / dvt2=2 / dvt3=3 on BLSAggregator0xF51c…8B13/ DVTValidator0x568b1486…. This replaces thePendingSlotCoSignerstub (which threw) with the real gossip-based BLS quorum co-signer that drives on-chain GToken slashing — the last piece of 目标2.Still default-off (
AUDIT_EXECUTE_SLASH). The stub remains the wiring default when disarmed.The safety model (this executes real slashes)
A node NEVER blindly signs a hash a peer sends. Each armed node independently:
messageHashfrom the structuredCoSignRequest(never trusts the requester's hash),epochblock and reconstructs theproofHash, and for the execute step requiresproofHash === evidenceHash(a substituted/innocent operator yields a different proofHash → refusal),The requester validates every response before counting it: recompute+match hash, cryptographic BLS verify, on-chain slot→key binding (uncompressed EIP-2537 compare + non-zero validator), dedup by slot. Strict threshold (WARNING 2/3, MINOR/MAJOR 3/3) — under-threshold → throw, never submitting a proof that would revert.
signerMask convention — PINNED
Verified against
BLSAggregator.sol:772(for slot 1..MAX: if ((signerMask >> (slot-1)) & 1)): slots→ bits-1.buildSignerMask([1,2,3]) = 7n. One tested helper.Files
gossip-quorum-cosigner.ts(+ spec, 20 tests) — responderverifyAndSign+ requestercoSign.gossip-cosign.spec.ts(8 transport tests).slash-consensus.ts— structuredCoSignRequestseam,buildSignerMask/recomputeMessageHash,MAX_VALIDATORS=13,QUORUM_COSIGNERtoken.gossip.{interfaces,service}.ts—cosign-request/response(point-to-point, ttl 0,requestIdcorrelation, freshmessageId); transport only, zero slash logic.blockchain.service.ts—getValidatorAtSlot/getBlsPublicKeyAtSlot/getSlotForValidator.audit.service.ts— structured call sites +verifyViolationForCoSignresponder verifier;audit.module.tsfactory provider (armed→real, disarmed→stub);configuration.tsco-sign knobs.Tests: 291/291 (+28). type-check / build / lint clean.
Confirmable ONLY against the 3 live deployed nodes
signerMask+ 256BsigG2accepted by the realBLSAggregator.verifyAndExecutepairing (EIP-2537 precompiles) +DVTValidator.queue/executeWithProof.messageHash/proofHash/epoch.getBLSPublicKeylive ABI tuple shape;getBlsPublicKeyAtSlotuncompressed equality vs live registered keys.Refs #163, CC-13.