Repository navigation
feat(identity): activate signature observability in drivers, with metrics and docs (3/3 of #2084) - #2441
feat(identity): activate signature observability in drivers, with metrics and docs (3/3 of #2084)#2441Effi-S wants to merge 3 commits into
Conversation
25ac61a to
f5b42ec
Compare
…ocabulary Signed-off-by: Effi-S <effi.szt@gmail.com>
…ion seam Split 2/3 of PR #2084. Adds the policy engine and makes the signature services instrumentable, while leaving the feature dormant (no driver wires it, so the observer stays Nop and the gate stays nil — no behavior change). - token/services/identity/throttle: Escalator (Observer + Gate), per-principal windowed state, level transitions, memory bounding, and Config with validated defaults. - token/services/identity/sigpolicy: Stack assembling metrics + audit + escalator behind the Reporter interface in one place. - provider.go / deserializer.go: SetObserver + instrumentation of signer/verifier resolution (Nop fast path, zero cost when unwired). - token/sig.go: the single gated surface — SignatureThrottled sentinel, WithSignatureObserver / WithSignatureGate / WithPrincipalKeyResolver, and the allow() helper. - common/tms.go, token/tms.go: SignatureInstrumentation plumbing so a driver can later install the stack. All of token/... builds with the drivers unchanged; new packages pass under -race. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Effi-S <effi.szt@gmail.com>
…rics and docs Split 3/3 of PR #2084 — the feature goes live. - token/services/identity/metrics.go (+ tests, golden, reference_test): the Prometheus sink implementing Observer + LevelGauge. Kept together because the metricsdoc self-test binds metrics.go to the golden, the metrics doc, and the driver wiring sites. - fabtoken/zkatdlog driver ws.go: build the sigpolicy.Stack, install the observer on the identity provider and the deserializer, return the stack with a transferred-guarded Stop() lifecycle. - fabtoken/zkatdlog driver.go: hand the validator its own un-observed NewDeserializer so ledger-named token owners can never feed the escalator (the core security fix), then install the stack on the client-facing service. - ttx/auditor.go: drop the now-dead SignatureThrottled branch. - docs: signature_observability.md plus configuration/metrics/grafana updates and the metrics golden. Full token/... builds; driver goleak tests, the identity metrics tests, and the metricsdoc reference test all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Effi-S <effi.szt@gmail.com>
f5b42ec to
baed98e
Compare
AkramBitar
left a comment
There was a problem hiding this comment.
Review: signature observability activation
Read every hunk with surrounding context and verified the branch builds and tests: go vet ./token/... clean; tests pass for metricsdoc, identity, sigpolicy, throttle, sigobserve and both driver packages; integration/ and the nested x/token/services/network/evm module build. I also checked the new goleak test has teeth — removing defer sigStack.Stop() in ws.go fails it with both Escalator.evictLoop and BucketSet.evictLoop leaked.
No compile- or crash-level bug. The transferred ownership pattern, the un-observed validator deserializer (the core security fix) and the nil guards in Metrics.Observe all check out.
9 findings inline: 4 medium, 5 low. The two I would not merge without a decision on are the mode: off documentation mismatch and the missing PrincipalKeyResolver on the Idemix driver — together they mean the knob operators are told to use does less than advertised at both ends of the range. The teardown-ordering one is a real fail-open, though only on an already-failing update path.
The redundancy findings on metrics.go are judgement calls, not defects — flagging them because two of the three new families are derivable from counters this PR leaves in place.
|
|
||
| **Parameter Descriptions:** | ||
|
|
||
| - **mode**: `off` (nothing metered, nothing denied), `monitor` (evaluate and report, never deny) or `enforce` (deny throttled principals) |
There was a problem hiding this comment.
medium — mode: off is documented here (and at docs/security/signature_observability.md:217) as "nothing metered, nothing denied", but only the denial half is true.
sigpolicy.New appends the metrics and audit observers unconditionally and gates only the escalator on cfg.Enabled(). Both drivers always pass a non-nil logger and a non-nil identity.Metrics (ws.go:59, ws.go:107), so instantiating the stack with Mode: off yields a sigobserve.multiObserver, not Nop (verified by probe).
Consequence: an operator who sets off specifically to shed overhead on a latency-sensitive node still pays, per signature operation, an Identity.UniqueID() hash, two time.Now() calls, 2-3 Prometheus family updates, an audit-logger level probe and an instrumentedVerifier allocation. The documented zero-cost d.observer == Nop fast path in common.Deserializer.getVerifier is never taken in production.
Either have off drop the sinks, or reword both docs to "policy off; instrumentation unaffected".
| NativeHistogramBucketFactor: 1.1, | ||
| NativeHistogramMaxBucketNumber: 100, | ||
| }), | ||
| SignerCacheLookups: p.NewCounter(metrics.CounterOpts{ |
There was a problem hiding this comment.
medium (redundancy) — identity_signer_cache_lookups_total carries no information that is not already exported.
It has exactly one producer, Timer.DoneResolution, called only at provider.go:410 — the same statement that already does SignerResolutions.With("outcome", path).Add(1). Since DoneResolution sets CacheHit = err == nil && path == PathCache:
{result="hit"}is identical toidentity_signer_resolutions_total{outcome="cache"}{result="miss"}is the sum of the other outcomes plus errors
So this adds a metric family, a golden-file entry, a docs row and per-call work that a recording rule over the existing counter would cover.
| Help: "Total number of signer/verifier service operations by operation, role and outcome", | ||
| LabelNames: []string{"network", "channel", "namespace", "op", "role", "outcome"}, | ||
| }), | ||
| SignatureOpDuration: p.NewHistogram(metrics.HistogramOpts{ |
There was a problem hiding this comment.
low (redundancy) — identity_signature_operation_duration_seconds{op="get_signer"} is sum without(path) of the pre-existing identity_get_signer_duration_seconds{path}, and both are observed from the same measurement at provider.go:406-410.
Each declares 13 explicit buckets plus native-histogram options, so GetSigner — on the endorsement hot path — now pays two full histogram observations for one timing. Worth dropping one, or deriving the aggregate in Prometheus.
| } | ||
| // The stack gates this service's client-facing signature service and is released when the | ||
| // service is done with. | ||
| service.SetSignatureInstrumentation(sigStack) |
There was a problem hiding this comment.
medium — activating the stack here means the gate runs on the Idemix driver, whose owner identities are pseudonyms, but no PrincipalKeyResolver is installed anywhere: WithPrincipalKeyResolver has zero call sites outside its own declaration in token/sig.go.
SignatureService.allow therefore keys on id.UniqueID(). A caller presenting a fresh nym per OwnerVerifier/GetAuditInfo call gets a brand-new full bucket every time and is never denied — exactly the failure mode token/sig.go:43-56 warns about.
This matters most for an operator following "Rolling out enforcement" (docs/security/signature_observability.md:235): switching to enforce on this driver looks like it has throttling but does not. Either wire a resolver or state the limitation in the rollout guide and in docs/configuration.md.
| } | ||
| // The stack gates this service's client-facing signature service and is released when the | ||
| // service is done with. | ||
| service.SetSignatureInstrumentation(sigStack) |
There was a problem hiding this comment.
medium — teardown ordering makes enforce fail open after a failed public-parameters update.
common.Service.Done() (token/core/common/tms.go:192, outside this diff) stops the signature instrumentation before the fallible walletService.Done(). If the wallet service's Done() fails — wallet/service.go:250 joins registry errors — then TMSProvider.Update (token/core/tms.go:182) returns that error and leaves the old service in m.services[key], and token.ManagementServiceProvider.Update (token/provider.go:131) returns before delete(p.services, key).
Result: the cached TMS keeps serving requests with an already-stopped — and therefore inert — gate, so enforcement silently stops denying until restart. Separately, the freshly built newService is dropped without Stop(), leaking its two eviction goroutines plus escalator/bucket state; before this PR a discarded token service held no goroutines.
Stopping the instrumentation only after the wallet teardown succeeds (or on the error path, stopping the new service) would close both.
| // identity provider and deserializer stay wired and keep reporting, while the throttle policy | ||
| // is left inert (a stopped stack neither denies nor accumulates state), so the escalator still | ||
| // referenced by that chain cannot grow state its released reaper could never reclaim. | ||
| sigStack.Stop() |
There was a problem hiding this comment.
low — the comment above correctly describes this as deliberate, so this is about the cost it leaves behind rather than a correctness bug.
On the wallet-only path (same at token/core/zkatdlog/nogh/v1/driver/ws.go:70) the stack is stopped but identityProvider.SetObserver / deserializer.SetObserver stay wired, so the observer is a multiObserver, not Nop. Every resolution hashes the identity and allocates an instrumentedVerifier for a policy that is inert, and since both factories pass &disabled.Provider{} the metrics go nowhere — the only live sink is the audit logger. Passing nil sinks to sigpolicy.New on this path would collapse the chain to Nop and keep the documented fast path.
One side effect worth a look regardless: Stack.Stop() zeroes identity_throttled_principals. Harmless with disabled.Provider, but zkatdlog's BaseWalletServiceFactory.NewWalletService is exported and promoted onto *Driver, so a caller passing a real TMS-scoped provider would clear a live TMS's gauge.
| provider respectively), so their events feed the metrics and the audit log. Only the | ||
| gate is bypassed. | ||
|
|
||
| Callers of `AuditorVerifier` must still be prepared for the error sentinel returned by a |
There was a problem hiding this comment.
low — this paragraph tells callers of AuditorVerifier to be ready for token.SignatureThrottled "from a gated verifier inside the deserializer", which contradicts line 42 of this same file ("the gate is consulted at exactly one place"), token/sig.go, and the comment this PR adds at token/services/ttx/auditor.go:304 ("this can never be a token.SignatureThrottled denial").
common.Deserializer holds an Observer, never a Gate, so the code comment is right and this paragraph is wrong. A reader following it adds unreachable error handling.
| | `identity_signer_resolutions_total` | counter | `outcome` (`cache`/`routed`/`fallback`) | How signers are being obtained. | | ||
| | `identity_get_signer_duration_seconds` | histogram | `path` | Latency of signer resolution per path. | | ||
|
|
||
| `op` is one of `get_signer`, `register_signer`, `register_identity_descriptor`, `is_me`, |
There was a problem hiding this comment.
low — escalation is listed among the op label values for identity_signature_operations_total, but Metrics.Observe returns early for OpEscalation (metrics.go:139), so identity_signature_operations_total{op="escalation"} is never produced — as this PR's own test asserts ("policy state must not be counted as a service call").
An operator writing sum by (op) queries from this list will look for a series that does not exist.
| const HeaderAuthorization = "Authorization" | ||
| const ( | ||
| IntermediaryRequestTimeout = 10 * time.Second | ||
| PayerAccessTokenExpiry = 60 * time.Minute |
There was a problem hiding this comment.
low (scope) — renaming the exported PayerAccessTokenExp to PayerAccessTokenExpiry and regrouping this const block is unrelated to signature observability.
Nothing breaks — the single in-repo user is updated — but it is a gratuitous exported-identifier change in a PR that is otherwise about the identity/driver layer, and it enlarges the diff for reviewers. Better as its own commit or PR.
Split 3/3 of #2084 - the feature goes live
token/services/identity/metrics.go(+ tests, golden,reference_test): the Prometheus sink implementingObserver+LevelGauge. Kept together because themetricsdocself-test bindsmetrics.goto the golden, the metrics doc, and the driver wiring sites.ws.go: build thesigpolicy.Stack, install the observer on the identity provider and the deserializer, return the stack with atransferred-guardedStop()lifecycle.driver.go: hand the validator its own un-observedNewDeserializerso ledger-named token owners can never feed the escalator (the core security fix), then install the stack on the client-facing service.ttx/auditor.go: drop the now-deadSignatureThrottledbranch.signature_observability.mdplus configuration/metrics/grafana updates and the metrics golden.