Skip to content

plugins/auth: add UCAN core library with EOA signer and MDBX store#20472

Merged
mh0lt merged 3 commits into
mainfrom
feat/componentization-auth
Apr 16, 2026
Merged

plugins/auth: add UCAN core library with EOA signer and MDBX store#20472
mh0lt merged 3 commits into
mainfrom
feat/componentization-auth

Conversation

@mh0lt

@mh0lt mh0lt commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the foundational auth plugin for the Cocoon plugin system.

UCAN core library (plugins/auth/)

  • Token creation, parsing, and chain verification (delegation chains)
  • Capability model with resource/action/policy matching (v1: equality operator)
  • EOA signer with ERC-191 personal_sign for BLS-free environments
  • MDBX-backed persistent token store with revocation support
  • Verifier checks revocation state when store is configured
  • Full test coverage (token lifecycle, capability matching with policies, chain verification, store operations)

Review feedback addressed

  • ComputeCID handles json.Marshal errors (returns zero-CID)
  • Covers evaluates Policy constraints (field + operator matching)
  • Chain depth guard uses >= (exact max, not off-by-one)
  • Verifier checks Store.IsRevoked before capability evaluation
  • NewVerifier accepts optional Store for revocation enforcement

Test plan

  • make lint passes
  • go test ./plugins/auth/... — all pass
  • CI passes

AskAlexSharov pushed a commit that referenced this pull request Apr 13, 2026
## 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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/auth UCAN primitives: Token, DID parsing, capabilities, chain verifier, and an ERC-191 EOA signer.
  • Adds persistence abstractions (Store, TokenResolver) plus MemoryStore and an MDBX-backed MDBXStore, with tests.
  • Adds node/nodebuilder and updates node/eth/backend.go to 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.

Comment thread plugins/auth/token.go
Comment on lines +90 to +94
Proofs: t.Proofs,
}
data, _ := json.Marshal(payload)
return sha256.Sum256(data)
}

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
// 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)
}

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread plugins/auth/verify.go Outdated
const maxChainDepth = 64

func (v *Verifier) verifyToken(ctx context.Context, token *Token, required Capability, depth int) error {
if depth > maxChainDepth {

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if depth > maxChainDepth {
if depth >= maxChainDepth {

Copilot uses AI. Check for mistakes.
Comment thread plugins/auth/store.go
Comment on lines +32 to +35
// 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)

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread plugins/auth/token.go
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

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Nonce []byte `json:"nonce"` // replay protection
Nonce []byte `json:"nonce"` // replay protection
Iat uint64 `json:"iat,omitempty"` // issued-at (unix timestamp, 0 = unspecified)

Copilot uses AI. Check for mistakes.
Comment thread plugins/auth/verify.go
Comment on lines +85 to +88
// 3. Signature
if err := v.verifySignature(ctx, token); err != nil {
return fmt.Errorf("signature verification failed: %w", err)
}

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread node/nodebuilder/builder.go Outdated
Comment on lines +34 to +54

"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{},

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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{},

Copilot uses AI. Check for mistakes.
Comment thread node/eth/backend.go Outdated
}
if s.downloaderProvider != nil {
s.downloaderProvider.Close()
if s.components.Downloader != nil {

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
if s.components.Downloader != nil {
if s.components != nil && s.components.Downloader != nil {

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +30
// 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{}

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@mh0lt mh0lt force-pushed the feat/componentization-downloader-only branch from 79f210b to 3dc03e1 Compare April 15, 2026 15:24
mh0lt added 2 commits April 15, 2026 15:24
…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.
@mh0lt mh0lt changed the base branch from feat/componentization-downloader-only to main April 15, 2026 15:25
@mh0lt mh0lt force-pushed the feat/componentization-auth branch from a859857 to 6b06a18 Compare April 15, 2026 15:25
mh0lt added a commit that referenced this pull request Apr 15, 2026
… 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.
@mh0lt mh0lt force-pushed the feat/componentization-auth branch from 2c0e428 to 152c7f8 Compare April 16, 2026 11:57
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.
@mh0lt mh0lt force-pushed the feat/componentization-auth branch from 152c7f8 to d8e5234 Compare April 16, 2026 12:04
@mh0lt mh0lt added this pull request to the merge queue Apr 16, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 16, 2026
@mh0lt mh0lt added this pull request to the merge queue Apr 16, 2026
Merged via the queue into main with commit 3fb9bac Apr 16, 2026
36 checks passed
@mh0lt mh0lt deleted the feat/componentization-auth branch April 16, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants