plugins/auth: add UCAN core library with EOA signer and MDBX store#20472
Conversation
## Summary Introduces `node/nodebuilder/Builder` as the central registry for extracted components. Replaces the inline `downloaderProvider` field on the Ethereum struct with `backend.components` — a single access point for all componentized subsystems. Currently holds only Downloader; Storage will be added in the next PR. ### Changes - **New:** `node/nodebuilder/builder.go` — Builder struct with `BuildDownloader` method - **Modified:** `node/eth/backend.go` — replace `downloaderProvider` field with `components *nodebuilder.Builder`, update all references This is minimal scaffolding that grows as components graduate from backend.go. ## Test plan - [x] `make lint` passes - [x] `make erigon` builds - [ ] CI passes **Depends on:** #20472 (auth plugin)
There was a problem hiding this comment.
Pull request overview
Adds a foundational UCAN-based authorization library under plugins/auth/ (tokens, DID parsing, capability model, delegation-chain verification, EOA signature verification, and MDBX-backed persistence), and refactors Erigon node initialization to use a central nodebuilder.Builder for extracted components (starting with the downloader).
Changes:
- Introduces
plugins/authUCAN primitives:Token, DID parsing, capabilities, chain verifier, and an ERC-191 EOA signer. - Adds persistence abstractions (
Store,TokenResolver) plusMemoryStoreand an MDBX-backedMDBXStore, with tests. - Adds
node/nodebuilderand updatesnode/eth/backend.goto initialize/close the downloader via the builder.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/auth/verify.go | UCAN token + delegation-chain verification logic |
| plugins/auth/token.go | Token model, payload encoding, CID computation, structure validation |
| plugins/auth/store.go | Store + in-memory store + Store→Resolver adapter |
| plugins/auth/store_mdbx.go | MDBX-backed Store implementation and table config |
| plugins/auth/store_mdbx_test.go | MDBX store tests |
| plugins/auth/signer_eoa.go | EOA (ERC-191 personal_sign) signature sign/verify implementation |
| plugins/auth/signer_eoa_test.go | EOA signer tests incl. full delegation chain with real signatures |
| plugins/auth/did.go | DID parsing + did:pkh constructor |
| plugins/auth/capability.go | Capability model (command path + policies) and matching helpers |
| plugins/auth/auth_test.go | Unit tests for DID, capabilities, tokens, verifier, and MemoryStore |
| plugins/auth/README.md | Module overview, current scope, and roadmap |
| node/nodebuilder/builder.go | New component registry/builder (currently downloader) |
| node/eth/backend.go | Switch downloader initialization/teardown to nodebuilder.Builder |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Proofs: t.Proofs, | ||
| } | ||
| data, _ := json.Marshal(payload) | ||
| return sha256.Sum256(data) | ||
| } |
There was a problem hiding this comment.
Token.ComputeCID ignores json.Marshal errors (data, _ := json.Marshal(...)). Because Policy.Value is typed as any, marshal can fail; in that case data will be nil and the CID becomes sha256(nil), causing deterministic collisions and potentially overwriting unrelated tokens in the store. Please handle the marshal error (e.g., return (CID, error) or reuse PayloadBytes() and propagate the error).
| // 1. The command is equal or a parent wildcard (e.g. /storage/* covers /storage/read) | ||
| // 2. All policies on this capability are satisfied by the required capability's context | ||
| func (c Capability) Covers(required Capability) bool { | ||
| return commandCovers(c.Command, required.Command) | ||
| } |
There was a problem hiding this comment.
Capability.Covers claims (in the comment) that policy constraints are evaluated, but the implementation only checks commandCovers and ignores Policy entirely. This means tokens with restrictive policies will currently authorize requests as if they had no policies, which can unintentionally broaden access. Either implement policy evaluation (and pass the required context) or update the API/docs to avoid implying policies are enforced.
| const maxChainDepth = 64 | ||
|
|
||
| func (v *Verifier) verifyToken(ctx context.Context, token *Token, required Capability, depth int) error { | ||
| if depth > maxChainDepth { |
There was a problem hiding this comment.
The chain depth guard uses if depth > maxChainDepth, which allows a maximum depth of maxChainDepth+1 (starting at depth=0) while the error message says "max 64". Consider using >= or renaming the constant to clarify whether it counts tokens vs edges.
| if depth > maxChainDepth { | |
| if depth >= maxChainDepth { |
| // IsRevoked checks if a token CID has been revoked, either directly | ||
| // or via a bulk revocation (all tokens from issuer before timestamp). | ||
| IsRevoked(ctx context.Context, cid CID, issuerDID string, issuedAt uint64) (bool, error) | ||
|
|
There was a problem hiding this comment.
Store.IsRevoked requires an issuedAt timestamp, but Token does not carry an issuance time (e.g., iat)—only nbf/exp. This makes it impossible for the verifier to reliably enforce bulk revocations using actual token data. Consider adding an explicit issued-at field to Token payload (and validating it) or changing the revocation API to use data that is available in the token.
| Subject string `json:"sub,omitempty"` // DID of the root authority | ||
| Command string `json:"cmd"` // capability command path | ||
| Policy []Policy `json:"pol,omitempty"` // capability constraints | ||
| Nonce []byte `json:"nonce"` // replay protection |
There was a problem hiding this comment.
Token struct does not include an issuance timestamp (iat/issuedAt), but the revocation API (Store.IsRevoked) expects callers to provide issuedAt for bulk revocations. To make revocation enforcement feasible, consider adding an iat field to the signed payload and using that consistently.
| Nonce []byte `json:"nonce"` // replay protection | |
| Nonce []byte `json:"nonce"` // replay protection | |
| Iat uint64 `json:"iat,omitempty"` // issued-at (unix timestamp, 0 = unspecified) |
| // 3. Signature | ||
| if err := v.verifySignature(ctx, token); err != nil { | ||
| return fmt.Errorf("signature verification failed: %w", err) | ||
| } |
There was a problem hiding this comment.
Verifier currently never consults revocation state (Store.IsRevoked / TableRevoked / TableRevokedAfter), so revoked tokens will still verify successfully. If revocation is part of the intended security model, add a revocation check as part of Verify/verifyToken (and ensure the token includes the timestamp information needed for bulk revocations).
|
|
||
| "github.com/erigontech/erigon/common/log/v3" | ||
| downloadercomp "github.com/erigontech/erigon/node/components/downloader" | ||
| "github.com/erigontech/erigon/node/ethconfig" | ||
|
|
||
| "github.com/erigontech/erigon/db/datadir" | ||
| "github.com/erigontech/erigon/db/downloader/downloadercfg" | ||
|
|
||
| "net/http" | ||
| ) | ||
|
|
||
| // Builder holds all extracted node component providers. | ||
| // Fields are added here as components graduate from backend.go. | ||
| type Builder struct { | ||
| Downloader *downloadercomp.Provider | ||
| } | ||
|
|
||
| // New allocates a Builder with all providers pre-initialized. | ||
| func New() *Builder { | ||
| return &Builder{ | ||
| Downloader: &downloadercomp.Provider{}, |
There was a problem hiding this comment.
Import block is not gofmt/goimports compliant: net/http (stdlib) is separated and placed after project imports. Please group stdlib imports together (context, net/http) before third-party imports to match Go formatting and avoid lint failures.
| "github.com/erigontech/erigon/common/log/v3" | |
| downloadercomp "github.com/erigontech/erigon/node/components/downloader" | |
| "github.com/erigontech/erigon/node/ethconfig" | |
| "github.com/erigontech/erigon/db/datadir" | |
| "github.com/erigontech/erigon/db/downloader/downloadercfg" | |
| "net/http" | |
| ) | |
| // Builder holds all extracted node component providers. | |
| // Fields are added here as components graduate from backend.go. | |
| type Builder struct { | |
| Downloader *downloadercomp.Provider | |
| } | |
| // New allocates a Builder with all providers pre-initialized. | |
| func New() *Builder { | |
| return &Builder{ | |
| Downloader: &downloadercomp.Provider{}, | |
| "net/http" | |
| "github.com/erigontech/erigon/common/log/v3" | |
| "github.com/erigontech/erigon/node/components/downloader" | |
| "github.com/erigontech/erigon/node/ethconfig" | |
| "github.com/erigontech/erigon/db/datadir" | |
| "github.com/erigontech/erigon/db/downloader/downloadercfg" | |
| ) | |
| // Builder holds all extracted node component providers. | |
| // Fields are added here as components graduate from backend.go. | |
| type Builder struct { | |
| Downloader *downloader.Provider | |
| } | |
| // New allocates a Builder with all providers pre-initialized. | |
| func New() *Builder { | |
| return &Builder{ | |
| Downloader: &downloader.Provider{}, |
| } | ||
| if s.downloaderProvider != nil { | ||
| s.downloaderProvider.Close() | ||
| if s.components.Downloader != nil { |
There was a problem hiding this comment.
Stop() dereferences s.components without checking if it is nil (if s.components.Downloader != nil { ... }). A defensive guard (s.components != nil && ...) would prevent a potential panic if Ethereum is ever constructed without initializing components (e.g., in tests or during future refactors).
| if s.components.Downloader != nil { | |
| if s.components != nil && s.components.Downloader != nil { |
| // EOASigner verifies UCAN token signatures from Ethereum EOA accounts. | ||
| // It uses ERC-191 personal_sign format: keccak256("\x19Ethereum Signed Message:\n32" + hash). | ||
| type EOASigner struct{} |
There was a problem hiding this comment.
PR description mentions an EOA signer with EIP-712 domain separation, but this implementation uses ERC-191 personal_sign. Please align the PR description/docs with the actual signing scheme, or switch the implementation to EIP-712 if that is required.
79f210b to
3dc03e1
Compare
…ification Initial implementation of the UCAN authorization plugin at plugins/auth/. This is the foundational auth layer for the cocoon plugin ecosystem (snapshot-manager, ccip, identity). Core types: - DID parsing (did:pkh for Ethereum addresses, did:key for standalone keys) - Capability model with hierarchical command matching and attenuation - UCAN Token with structure validation, time bounds, CID computation - Delegation chain verifier (recursive proof resolution, attenuation checks) - Store interface with MemoryStore for testing (MDBX backend to follow) 14 tests covering: DID parsing, capability matching, attenuation rules, token validation, delegation chains (root + 2-level), broken chains, expired tokens, and revocation (individual + bulk). No Erigon internal dependencies — pure library code. Design: https://github.com/erigontech/cocoon/tree/master/pocs/auth
- EOA Signer: real ECDSA signature creation and verification using ERC-191 personal_sign format. Recovers address from signature and matches against the did:pkh issuer DID. - MDBX Store: persistent token and revocation storage backed by MDBX. Three tables: AuthTokens (CID→token), AuthRevoked (CID→revoked), AuthRevokedAfter (issuerHash→timestamp for bulk revocation). - README: documents what's implemented, what's next, and future config. - Full delegation chain test with real ECDSA signatures (Alice→Bob→Carol) including tamper detection. 21 tests total, all passing.
a859857 to
6b06a18
Compare
… commit
The glamsterdam suite was tracking upstream
ethpandaops/ethereum-package@main unpinned, while other suites
(regular, pectra) pin to a specific version. Upstream commit 835dd9b
("feat: support gpu ere prover in zkboost", 2026-04-15) introduced an
undefined GpuConfig reference in zkboost_launcher.star:272, breaking
every Erigon PR that ran glamsterdam from that point on — regardless
of what the PR changed. Six unrelated PRs (#20471, #20472, #20526,
#20583, #20584, #20585) all failed identically.
Pin to e07503d16b (2026-04-13, the commit before the break) to stop
the bleeding. This was the last state under which our main successfully
ran glamsterdam. Revisit once upstream stabilises, or switch to a
tagged release that supports gloas_fork_epoch / fulu_fork_epoch which
our glamsterdam.io config requires.
2c0e428 to
152c7f8
Compare
Six issues from the automated review:
1. token.go: ComputeCID now returns zero-CID on json.Marshal error
instead of silently hashing nil bytes.
2. capability.go: Covers now evaluates Policy constraints — each
granting policy must be matched by the required capability with
the same field and a satisfying value. V1 supports equality ("eq")
operator only; range operators are a future extension.
3. verify.go: chain depth guard changed from > to >= so maximum
depth is exactly maxChainDepth (64), matching the error message.
4. verify.go: Verifier now checks revocation state via Store.IsRevoked
when a store is configured. Revoked tokens are rejected before
capability evaluation.
5. verify.go: NewVerifier takes an optional Store parameter. Existing
callers updated to pass their store (enables revocation) or nil
(no revocation check).
6. signer_eoa.go: code correctly uses ERC-191 personal_sign (not
EIP-712 as the original PR description stated). PR description
to be updated.
152c7f8 to
d8e5234
Compare
Summary
Adds the foundational auth plugin for the Cocoon plugin system.
UCAN core library (
plugins/auth/)Review feedback addressed
ComputeCIDhandles json.Marshal errors (returns zero-CID)Coversevaluates Policy constraints (field + operator matching)>=(exact max, not off-by-one)Store.IsRevokedbefore capability evaluationNewVerifieraccepts optional Store for revocation enforcementTest plan
make lintpassesgo test ./plugins/auth/...— all pass