Add Autobahn fullnode (CON-309)#3525
Conversation
Adds an autobahn-role config (validator|rpc-only). With autobahn-role= "rpc-only", a non-validator RPC node loads the committee from autobahn.json as a routing table only — no consensus participation, no block execution, no validator key required. eth_sendRawTransaction submitted to such a node is recovered, sender-shard- mapped via Committee.EvmShard, and forwarded over HTTP to the shard owner's EVM RPC. The rest of the giga stack (consensus, producer, data, service) stays nil; Run is a no-op; block-read methods return a sentinel error. InitRPCOnly bootstraps the app once at startup so x/evm params (chain ID, signer config) are populated. app.go pre-fires the EVM HTTP/WS start gate since rpc-only nodes don't call ProcessBlock in the current milestone — see TODO(autobahn-read-path) in NewGigaRouter for the read-side scope. CI: wires PR #3234's make autobahn-integration-test into the workflow as a new top-level job (it owns its own cluster via TestMain, so can't share the matrix's cluster), and adds a TestAutobahn/RPCOnlyForwarding sub-test that verifies an actual signed tx round-trips through the proxy: rpc-only sidecar → shard owner → block inclusion → receipt on validator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR SummaryHigh Risk Overview GigaRouter split: Config & ops: CI & tests: New Reviewed by Cursor Bugbot for commit b8e9d21. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3525 +/- ##
==========================================
- Coverage 59.12% 58.23% -0.90%
==========================================
Files 2213 2141 -72
Lines 182814 174592 -8222
==========================================
- Hits 108096 101673 -6423
+ Misses 64993 63860 -1133
+ Partials 9725 9059 -666
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Guard autobahnRPCOnly on AutobahnConfigFile != "" so a stray autobahn-role without a config file doesn't pre-fire the EVM gate (matches node.go's gigaRPCOnly construction and the AutobahnRole godoc, which already says the role is ignored when the config file is empty). - Drop time.Sleep + time.After from the rpc-only Run-cancel test; a pre-cancelled context proves the unblock path without any goroutine- timing synchronization. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ProcessBlock's deferred gate-fire didn't cover rpc-only nodes because they never execute a block. Factor the gate-fire into a helper and call it from InitChainer as well — fresh-start validators reach it via the handshaker / runExecute InitChain call, rpc-only nodes via GigaRouter.InitRPCOnly. Both paths converge on the same chain event. The *Sent flags keep the second fire a no-op. Drops the autobahnRPCOnly field on App and the consensus-mode branch in RegisterLocalServices that bugbot flagged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bugbot caught the leftover copy-paste from buildGigaConfig — the rpc-only variant intentionally skips the membership check, so nodeKey was never read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bot caught a latent issue: InitRPCOnly's early-return (when the app already has committed state) skipped the InitChain call, so the InitChainer defer never fired and the EVM RPC goroutines would block forever. Today the path is unreachable (rpc-only never commits) but read-side scope changes that. Wrap BaseApp.Info to also fire the gate when LastBlockHeight > 0. The trigger is the app's own committed state, not a consensus-engine flag, so no cross-layer branching. Pairs naturally with InitChainer's defer: fresh start fires via InitChain, restart-with-state via Info, steady- state via ProcessBlock. Verified: make autobahn-integration-test passes all 6 sub-tests including RPCOnlyForwarding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bugbot caught the validator-address-map construction was copy-pasted between buildGigaConfig and buildRPCOnlyGigaConfig. Pull it into a single loadAutobahnCommittee helper that returns the parsed file config + the committee map; both callers compose the rest of their config from there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The override is a sei-tendermint-specific concession (rpc-only nodes have no ProcessBlock to fire the gate from), not a general improvement to Info. Calling that out in the header so a future reader doesn't wonder why we touched an ABCI method that looks innocent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review caught that the unconditional Info wrapper fires the gate before CometBFT Handshaker's ReplayBlocks runs, binding EVM HTTP/WS while replay is mid-flight — strictly worse staleness window than the original ProcessBlock-defer trigger (which fires after the first replayed block commits). Re-introduce autobahnRPCOnly as a single bool on App, set from tmConfig (guarded on AutobahnConfigFile != ""), and gate the Info-side fire on it. Autobahn nodes skip the Handshaker entirely, so the gating is also what makes the override safe for the mode it exists for. Also addresses smaller review feedback: - LastCommittedBlockNumber: reword to match /status's actual committed > 0 guard; the "LastCommitted >= Latest" framing was overstated and fragile. - rpc_only_test.go: rename identical-string constants to expose the routing intent at call sites (validatorEVMRPCURLOnHost vs evmRPCURLOnContainerLocalhost — one is host-curled, the other goes through docker exec into the rpc-only sidecar). Verified: make autobahn-integration-test passes all 6 sub-tests including RPCOnlyForwarding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI fresh-cluster runs failed at TestAutobahn/RPCOnlyForwarding with
"sei_associate error: : unknown" because the V/R/S hex encoding
differed from what `seid tx evm associate-address` produces:
crypto.Sign returns sig[64] as a raw byte and hex.EncodeToString of
[]byte{0x00} produces "00", but the CLI uses big.Int.Bytes() which
strips leading zeros to "" for V=0. The chain's signature
verification rejects the encoding mismatch (CheckTx code != 0).
Match the CLI exactly: round-trip through big.Int.Bytes() for R, S,
and V. Local runs previously passed because they hit the
"balance > 0 → skip associate" branch against a long-lived cluster
where admin was already associated from prior runs.
Also silence `git describe --tags 2>/dev/null` in Makefile — the
"fatal: No names found, cannot describe anything" line was cluttering
every CI log and surfaced from a shallow clone with no tags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app/app.go: replace racy *Sent flag pair on signalEVMRPCReady with sync.Once. The Info-side fire site makes the race reachable from any concurrent /abci_info RPC call once read-path lands; cheap to fix now. Also restore the InitChainer doc comment that an earlier edit orphaned onto the helper above it. - sei-tendermint/internal/p2p/giga_router.go: change LastCommittedBlockNumber's rpc-only sentinel from -1 to 0 so /status's JSON response carries a non-negative integer for downstream clients. status.go's "committed > 0" guard still silently skips it, so the invariant warning stays quiet. Update unit test. - integration_test/autobahn: fold teardownRPCOnlyNode into teardownCluster. TestMain no longer repeats the kill in the two error paths + the success path; adding future sidecars goes in the same centralized teardown. Verified: make autobahn-integration-test passes all 6 sub-tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| httpServerStartSignalSent bool | ||
| wsServerStartSignalSent bool | ||
| httpServerStartSignal chan struct{} | ||
| wsServerStartSignal chan struct{} |
There was a problem hiding this comment.
I wonder if these really are intended to be independent, or we can have just serverStartSignal channel that is closed via evmRPCReadyOnce. Optionally, note that you can wrap evmRPCReadyOnce and serverStartSignal into one sei-tendermint/libs/utils.Once object.
There was a problem hiding this comment.
We don't need to touch app.go now we are doing read side.
| // eth_sendRawTransaction to the shard | ||
| // owner. No consensus participation. | ||
| // Ignored when AutobahnConfigFile is empty. | ||
| AutobahnRole string `mapstructure:"autobahn-role"` |
There was a problem hiding this comment.
nit: shouldn't this be conditioned on the presence of the validator key instead?
| // needed; leaving them all out for now keeps the write-only milestone | ||
| // minimal and the read-path requirements explicit. | ||
| logger.Info("GigaRouter initialized (rpc-only)", "validators", len(cfg.ValidatorAddrs)) | ||
| return &GigaRouter{ |
There was a problem hiding this comment.
if GigaRouter is supposed to support both modes, all the validator-only fields should be put into an utils.Option section of the GigaRouter struct.
| // FinalizeBlock responses are not stored on disk) without reaching into | ||
| // the unexported router.cfg. | ||
| func (r *GigaRouter) MaxGasPerBlock() int64 { | ||
| if r.cfg.RPCOnly { |
There was a problem hiding this comment.
the pattern of returning dummy values depending on the mode is not type-safe. Neither is returning errors in case the mode is incompatible. For example, instead, you can create a wrapper type sth like GigaValidatorRouter{}, and a cast method: GigaRouter.AsValidator() Option[GigaValidatorRouter], and move all validator-only methods there. This is just an example, there are probably other safe patters available as well. The general idea is to conditionally provide a wider role-specific interface, instead of custom mismatch handling in each method.
| // before RPC begins serving. See the TODO(autobahn-read-path) in | ||
| // NewGigaRouter for the loops the read side will pull back in. | ||
| <-ctx.Done() | ||
| return ctx.Err() |
There was a problem hiding this comment.
"return nil" would be totally fine. There is no obligation for Run to block if it has nothing to do.
|
I might be misunderstanding the goal here, but if the point is to support just RPC forwarding endpoint, then it should not create GigaRouter (and App?) at all. If this is a stop gap measure until we support full-nodes - nodes actually processing blocks, then I'd like to understand why we cannot implement it rn (i.e. skip the stop gap). |
Fresh-eyes review noted the prior framing overstated the handshaker- replay concern: autobahn nodes skip the Handshaker entirely, so that risk applies to non-autobahn CometBFT validators instead — which is precisely what the autobahnRPCOnly guard scopes out. Rephrase to center the guard's actual purpose (scoping the fire to rpc-only) rather than the autobahn-specific replay risk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cache the resolved *producer.Config on gigaValidatorRouter so MaxGasPerBlock doesn't re-Option-unwrap on every RPC call, and extract spawnReadPath onto gigaRouterCommon so both Run methods share the data/execute/service spawns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Config.AutobahnMaxInboundRPCOnlyPeers becomes *int (matches the existing TOML pointer convention used by MaxOutboundConnections); the programmatic GigaRouterConfig.MaxInboundRPCOnlyPeers becomes utils.Option[int] (matches the rest of the codebase's Option usage). A small intPtrToOption helper bridges at the setup.go seam. Effect for operators: leaving the TOML key absent uses the built-in default (10); setting it to 0 now means "reject all inbound rpc-only block-sync from this validator" instead of being silently coerced to the default. The rendered TOML template ships the line commented out, matching unsafe-bypass-commit-timeout-override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| state *consensus.State | ||
| data *data.State | ||
| // state is the validator-mode consensus state. nil in block-sync-only mode. | ||
| state *consensus.State |
| // subscriber and executes them locally; it does not run consensus or | ||
| // producer, does not accept inbound giga connections, and sources its | ||
| // "last committed block" from data.State directly. | ||
| type gigaRPCOnlyRouter struct { |
There was a problem hiding this comment.
can we stick to fullnode name instead of RPConly? This is the naming in tendermint and afaik also the industry standard (I might be wrong)
| // Producer is only set on validator paths; rpc-only nodes don't | ||
| // produce blocks and source MaxGasPerBlock from genesis instead. | ||
| Producer utils.Option[*producer.Config] | ||
| TxMempool *mempool.TxMempool |
There was a problem hiding this comment.
fullnodes are not supposed to have mempool
| @@ -42,20 +42,90 @@ type GigaRouterConfig struct { | |||
| DialInterval time.Duration | |||
| ValidatorAddrs map[atypes.PublicKey]GigaNodeAddr | |||
| Consensus *consensus.Config | |||
There was a problem hiding this comment.
fullnodes should not need consensus config (they need committee, but that's currently derived from ValidatorAddrs anyway).
| return nil, fmt.Errorf("data.NewState(): %w", err) | ||
| } | ||
| if cfg.RPCOnly { | ||
| // Every committee member must expose an EVMRPC URL: rpc-only nodes |
There was a problem hiding this comment.
I think we should require all urls in both validator and fullnode modes by now. I have added a fallback to local mempool just for the sake of easier tests, but with fullnode in the picture it won't help with anything anymore.
|
|
||
| // Subscribe once here (takes avail's internal lock once); subsequent | ||
| // Load() calls from RPC handlers are lock-free atomic pointer reads. | ||
| producerConfig := cfg.Producer.OrPanic("validator-mode requires GigaRouterConfig.Producer") |
| key NodeSecretKey | ||
| data *data.State | ||
| service *giga.Service | ||
| poolOut *giga.Pool[NodePublicKey, rpc.Client[giga.API]] |
There was a problem hiding this comment.
non-validator connections are supposed to create a mesh therefore:
- non-validator nodes should also have both inbound and outbound connections
- blocksyncing should be allowed in both directions by default (that's why I didn't want to restrict sending blocks from non-validators -> validators for now, we can add this as a separate feature).
There was a problem hiding this comment.
That's right, it would be more like a tree, but I agree that they do need both inbound and outbound connections, I just didn't want to do that in this one PR, because this PR would be too big. But maybe we should keep poolIn and poolOut.
| consensus *consensus.State | ||
| producer *producer.State | ||
| producerConfig *producer.Config | ||
| poolIn *giga.Pool[NodePublicKey, rpc.Server[giga.API]] |
There was a problem hiding this comment.
fyi, the giga.Pool concept was supposed to support attaching custom tasks within the lifetime of each connection, but it does complicate stuff and the fancy features are unused by now, so soon I think I'll simplify it, unless a use case arises somewhere.
| // concurrent non-committee inbound block-sync connections. | ||
| // Non-blocking acquire (select + default) so excess peers get a clean | ||
| // rejection at handshake time rather than queuing. | ||
| inboundRPCOnlyPermits chan struct{} |
There was a problem hiding this comment.
| // Load() calls from RPC handlers are lock-free atomic pointer reads. | ||
| producerConfig := cfg.Producer.OrPanic("validator-mode requires GigaRouterConfig.Producer") | ||
| producerState := producer.NewState(producerConfig, cfg.TxMempool, consensusState) | ||
| // None → built-in default; Some(0) → reject all (channel cap 0); Some(n>0) → operator override. |
There was a problem hiding this comment.
nit: let's push this default outside of p2p to config/config.go and require a concrete number in giga_router.go
| } | ||
|
|
||
| func NewGigaRouter(cfg *GigaRouterConfig, key NodeSecretKey) (*GigaRouter, error) { | ||
| func NewGigaRouter(cfg *GigaRouterConfig, key NodeSecretKey) (GigaRouter, error) { |
There was a problem hiding this comment.
how about we have separate constructors and separate config types? I suspect it may make the code less cluttered.
There was a problem hiding this comment.
gigaFullNodeConfig may be embedded in gigaValidatorConfig, for example.
| // giga service (block fetcher). Mode-specific spawns (dial loop + | ||
| // consensus/producer on the validator path; the single-active subscriber | ||
| // on the rpc-only path) live at each Run call site. | ||
| func (r *gigaRouterCommon) spawnReadPath(ctx context.Context, s scope.Scope) { |
There was a problem hiding this comment.
nit: scope by design is not supposed to be passed to subroutines if avoidable, because ctx is strictly bound to scope, while it is not possible to enforce this relation in the function signature. In this case it should be perfectly fine to implement runReadPath() which uses scope internally.
| return fmt.Errorf("not a SeiGiga connection") | ||
| } | ||
| // Filter unwanded connections. | ||
| key := hConn.msg.NodeAuth.Key() |
There was a problem hiding this comment.
make Key optional in handshake.
| return fmt.Errorf("peer not whitelisted") | ||
| if !isCommittee { | ||
| select { | ||
| case r.inboundRPCOnlyPermits <- struct{}{}: |
There was a problem hiding this comment.
nit: here you don't even need a blocking semaphore, just atomic update on pool size would be good enough.
| } | ||
|
|
||
| func (r *GigaRouter) EvmProxy(sender common.Address) (*url.URL, bool) { | ||
| func (r *gigaValidatorRouter) EvmProxy(sender common.Address) (*url.URL, bool) { |
| // to pull finalized blocks from committee members. NewGigaRouter returns | ||
| // the rpc-only concrete impl through the interface; assert on the | ||
| // concrete type so we can inspect the shared common fields. | ||
| require.True(t, routerIface.IsRPCOnly()) |
There was a problem hiding this comment.
if the only purpose of IsRPCOnly is to guard type cast, then it is not useful.
| require.True(t, routerIface.IsRPCOnly()) | ||
| router, ok := routerIface.(*gigaRPCOnlyRouter) | ||
| require.True(t, ok, "rpc-only NewGigaRouter should return *gigaRPCOnlyRouter") | ||
| require.NotNil(t, router.data) |
There was a problem hiding this comment.
do we really need those non-nil checks?
Match the tendermint / industry term for non-validator nodes. Pure
rename: all identifiers (RPCOnly, rpcOnly, rpc-only, Rpc-only, rpc_only)
become Fullnode/fullnode variants; the giga_router_rpc_only{,_test}.go
files are git-mv'd to giga_router_fullnode{,_test}.go. Awkward
"Fullnode nodes" phrasing from the noun-now-adjective shift is
collapsed to "fullnodes". No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Require EVMRPC on every committee member on both validator and fullnode paths (the silent-drop branch of EvmProxy's .Get() is now unreachable in production). Move the self-shard short-circuit from the validator-only EvmProxy into a common method on gigaRouterCommon, gated by an Option[atypes.PublicKey] validatorKey field — None on fullnodes, Some on validators. Both per-impl EvmProxy overrides go away. Tests updated to give every committee member an EVMRPC URL; the missing- URL branch is no longer reachable from the test fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the unified GigaRouterConfig + NewGigaRouter dispatch with
separate config types and constructors:
- GigaRouterCommonConfig: DialInterval, ValidatorAddrs,
PersistentStateDir, App (*proxy.Proxy), GenDoc.
- GigaFullnodeConfig: embeds the common config, nothing else.
- GigaValidatorConfig: embeds the common config; adds ValidatorKey,
ViewTimeout, Producer, TxMempool, MaxInboundFullnodePeers.
- NewGigaFullnodeRouter / NewGigaValidatorRouter return the concrete
types so internal callers can reach validator-only operations
without a runtime cast that returns an error.
GigaRouter interface shrinks to the read-path + Run + EvmProxy. The
IsFullnode method and RunInboundConn-on-fullnode (which only returned
an error) both go away; router.go type-asserts to *gigaValidatorRouter
at the inbound-handshake site instead.
executeBlock now reads the ABCI app from cfg.App (passed through the
config rather than fetched via TxMempool.App()), and the
Lock/Unlock/Update mempool ops are gated on an Option txMempool —
None on fullnodes (no local CheckTx, nothing to evict).
createRouter dispatches on whether a local validator key is present
rather than on cfg.Mode == ModeFull; node.go threads Some/None into
gigaValidatorKey based on the same property. IsAutobahnFullnode is
removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep of smaller items: - giga.Service.state becomes utils.Option[*consensus.State]; the block-sync-only mode (None on fullnodes) is now type-visible rather than a "nil in fullnode mode" comment. consensus/avail handlers reach the state via a consensusState() accessor that OrPanics on None — kept concentrated rather than scattered across every dereference. - DefaultAutobahnMaxInboundFullnodePeers moves to config/config.go; GigaValidatorConfig.MaxInboundFullnodePeers becomes a plain int that the caller (setup.go) must populate, with a default-resolving helper in setup. giga_router no longer carries an operator-facing default. - Inbound-cap admission switches from a buffered-channel semaphore to an atomic.Int32 counter — Add(1)/check/Add(-1) on the hot path. Brief overshoot under contention is benign (over-reject one or two peers, never over-accept). - Drop tautological non-nil assertions from the fullnode test — if the constructor returned ok, the fields are populated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the consensusState() helper and Option[*consensus.State] field from the phase-3 cleanup. The Option doesn't catch a bug at compile time — Go's type system can't see "this method is only spawned by RunServer/RunClient" — so the OrPanic at every consensus/avail dereference guarded a code path that's already gated upstream at the spawn-list level. Keeping the plain *consensus.State field and a sharper doc comment expresses the same invariant with ~30 fewer LoC. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address the two latest cursor[bot] findings on giga_router_fullnode.go: (Medium) LastCommittedBlockNumber on the fullnode returned data.NextBlock()-1, the data layer's QC/receive frontier, which can be ahead of the executeBlock loop's actual app-Commit frontier — clients querying /status then hitting receipts/balances at the reported height could see "not found" during catch-up. The validator side had the same shape (read from consensus.CommitQC) just with a smaller gap. Both impls now read a single lastExecutedBlock atomic.Int64 on gigaRouterCommon, seeded from app.Info().LastBlockHeight at runExecute startup and stored after every successful executeBlock — matches the CometBFT /status semantic. The validator's lastCommitQCRecv subscription is no longer needed and goes away with the override. (Low) Empty ValidatorAddrs would let runFullnodeSubscriber hit a modulo-by-zero on `(i + 1) % len(addrs)`. AutobahnFileConfig.Validate and NewRoundRobinElection both reject this upstream, but the bot's point about direct GigaRouterConfig construction was fair — validateCommonAndBuildData now has an explicit non-empty guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bf1f20f. Configure here.
| cp docker/rpcnode/config/app.toml ~/.sei/config/app.toml | ||
| cp docker/rpcnode/config/config.toml ~/.sei/config/config.toml | ||
| cp build/generated/genesis.json ~/.sei/config/genesis.json | ||
| cp "$GENESIS_SRC" ~/.sei/config/genesis.json |
There was a problem hiding this comment.
Genesis wait never fails script
Medium Severity
After the five-minute poll, if build/generated/genesis.json is still missing, the script continues and cp fails, but without set -e initialization may proceed with no valid genesis instead of exiting clearly.
Reviewed by Cursor Bugbot for commit bf1f20f. Configure here.
| _ = runMake(nil, "kill-rpc-node") // best-effort cleanup | ||
|
|
||
| cmd := exec.Command("make", "run-rpc-node-skipbuild") | ||
| cmd.Env = append(os.Environ(), "AUTOBAHN=true", "CLUSTER_SIZE=4") |
There was a problem hiding this comment.
Hardcoded cluster size four
Low Severity
setupFullnodeNode always passes CLUSTER_SIZE=4 to gen-autobahn-config, while setupCluster discovers the real validator count dynamically. A non-four-node cluster gets a committee JSON that does not match running validators.
Reviewed by Cursor Bugbot for commit bf1f20f. Configure here.
Address cursor[bot]'s "role uses mode only" finding: previously the
giga dispatch was on validatorKey presence at the setup.go seam, but
node.go populated the Option from cfg.Mode == ModeValidator — so in
observable behaviour the role was still flag-driven, not key-driven.
Now node.go always hands setup.go the local validator key when
Autobahn is on, and buildAndStartGigaRouter checks whether that key is
in autobahn.json's committee. In committee → validator path. Not in
committee → fullnode path with a clear INFO log spelling out which
branch was taken and why ("no local validator key" vs "local validator
key is not in the committee"). Mode is no longer consulted for the
autobahn role.
The remote-signer-not-supported check moves alongside the dispatch so
a fullnode-by-virtue-of-not-being-in-committee isn't penalised for
having priv-validator.laddr set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>


Summary
Adds a non-validator fullnode role to Autobahn. A fullnode loads the committee from
autobahn.jsonas a routing table, dials a single committee member at a time over giga to pull finalized blocks (StreamFullCommitQCs+GetBlock), and runs the localrunExecuteloop.Role is decided by committee membership, not by
mode: the node provides its local validator key, and the giga setup picksNewGigaValidatorRouteriff that key is inautobahn.json. Otherwise it starts as a fullnode (logger.Info "Autobahn: ... starting as fullnode").cfg.Modeis intentionally not consulted for the autobahn role — operators don't need a separate flag that could desync from the on-chain committee. The remote-signer-not-supported check moved alongside the dispatch so a fullnode-by-virtue-of-not-being-in-committee isn't penalised for havingpriv-validator.laddrset.Type split.
GigaRouteris now built by two separate constructors:NewGigaValidatorRouter(*GigaValidatorConfig, ...)— committee members. Embeds the common config and addsValidatorKey,ViewTimeout,Producer,TxMempool,MaxInboundFullnodePeers. Runs consensus + producer + the full giga service, accepts inbound peers.NewGigaFullnodeRouter(*GigaFullnodeConfig, ...)— non-validators. Embeds the common config only. No consensus, no producer, no mempool. Pulls blocks via a single-active-subscriber dial loop (shuffled committee, exponential backoff capped at 5 min, time-since-healthy gate).Validator-only operations (
RunInboundConn) live on*gigaValidatorRouterand are reached via in-package type assertion at the router-inbound handshake site — theGigaRouterinterface intentionally exposes only the read path (Run,LastCommittedBlockNumber,MaxGasPerBlock,BlockByNumber,BlockByHash,EvmProxy), so calling a validator-only method on a fullnode is a compile error rather than a runtime error-return.EVM tx writes.
eth_sendRawTransactionis sender-shard-mapped viaCommittee.EvmShardand forwarded over HTTP to the shard owner. Every committee member must expose anevmrpcURL on both validator and fullnode paths —NewGigaXxxRouterrejects configs where any URL is missing so the silent-drop branch ofEvmProxyis unreachable in production. Validators short-circuit on their own shard to local mempool; fullnodes always forward.Inbound rpc-peer admission. Validators serve the full giga RPC to committee peers and the block-sync subset (
StreamFullCommitQCs+GetBlockonly) to non-committee peers, capped per-validator byautobahn-max-inbound-fullnode-peers(default10; set0to reject all inbound fullnode block-sync;*intpointer in TOML so absent and explicit-zero are distinguishable). The cap is anatomic.Int32counter on the hot path.LastCommittedBlockNumberreports executed height. Both validator and fullnode read a singlelastExecutedBlock atomic.Int64updated after everyexecuteBlockcommit, matching CometBFT/statussemantics — clients querying receipts at the reported height never see a height the app hasn't reached.Config.
autobahn-config-fileandautobahn-max-inbound-fullnode-peersare top-level TOML keys (placed above any[section]header so viper sees them at root scope where mapstructure expects).PersistentStateDiris rootified against the node's--homedir, matching how cometbft handles relative paths elsewhere.Note: fullnode-to-fullnode mesh (bidirectional block sync between non-validators) is deferred to a follow-up PR — fullnodes here are pull-only against committee members.
Test plan
gofmt -s+go vetcleango test ./sei-tendermint/internal/p2p/... ./sei-tendermint/config/... ./sei-tendermint/node/...passmake autobahn-integration-test: the whole client-facing EVM flow (balance / chainId / nonce / send / receipt) runs through a fullnode sidecar that catches up via giga and forwards writes; halt/liveness/permanent-fault scenarios verified by polling helpers against the fullnode's CometBFT RPC.🤖 Generated with Claude Code