Skip to content

Add Autobahn fullnode (CON-309)#3525

Open
wen-coding wants to merge 46 commits into
mainfrom
wen/autobahn_rpc_write_side
Open

Add Autobahn fullnode (CON-309)#3525
wen-coding wants to merge 46 commits into
mainfrom
wen/autobahn_rpc_write_side

Conversation

@wen-coding
Copy link
Copy Markdown
Contributor

@wen-coding wen-coding commented May 30, 2026

Summary

Adds a non-validator fullnode role to Autobahn. A fullnode loads the committee from autobahn.json as a routing table, dials a single committee member at a time over giga to pull finalized blocks (StreamFullCommitQCs + GetBlock), and runs the local runExecute loop.

Role is decided by committee membership, not by mode: the node provides its local validator key, and the giga setup picks NewGigaValidatorRouter iff that key is in autobahn.json. Otherwise it starts as a fullnode (logger.Info "Autobahn: ... starting as fullnode"). cfg.Mode is 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 having priv-validator.laddr set.

Type split. GigaRouter is now built by two separate constructors:

  • NewGigaValidatorRouter(*GigaValidatorConfig, ...) — committee members. Embeds the common config and adds ValidatorKey, 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 *gigaValidatorRouter and are reached via in-package type assertion at the router-inbound handshake site — the GigaRouter interface 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_sendRawTransaction is sender-shard-mapped via Committee.EvmShard and forwarded over HTTP to the shard owner. Every committee member must expose an evmrpc URL on both validator and fullnode paths — NewGigaXxxRouter rejects configs where any URL is missing so the silent-drop branch of EvmProxy is 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 + GetBlock only) to non-committee peers, capped per-validator by autobahn-max-inbound-fullnode-peers (default 10; set 0 to reject all inbound fullnode block-sync; *int pointer in TOML so absent and explicit-zero are distinguishable). The cap is an atomic.Int32 counter on the hot path.

LastCommittedBlockNumber reports executed height. Both validator and fullnode read a single lastExecutedBlock atomic.Int64 updated after every executeBlock commit, matching CometBFT /status semantics — clients querying receipts at the reported height never see a height the app hasn't reached.

Config. autobahn-config-file and autobahn-max-inbound-fullnode-peers are top-level TOML keys (placed above any [section] header so viper sees them at root scope where mapstructure expects). PersistentStateDir is rootified against the node's --home dir, 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 vet clean
  • go test ./sei-tendermint/internal/p2p/... ./sei-tendermint/config/... ./sei-tendermint/node/... pass
  • make 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.
  • CI green

🤖 Generated with Claude Code

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>
@cursor
Copy link
Copy Markdown

cursor Bot commented May 30, 2026

PR Summary

High Risk
Large consensus/P2P/RPC surface (router split, block execution, EVM proxy, status height semantics) with new integration paths; mistakes affect liveness, forwarding, and client-visible chain height.

Overview
Introduces an Autobahn fullnode path so non-committee nodes can sync finalized blocks from validators, execute them locally, and forward EVM writes to the correct shard owner—without running consensus or block production.

GigaRouter split: NewGigaValidatorRouter vs NewGigaFullnodeRouter, with role chosen by committee membership (local key in autobahn.json), not mode. Fullnodes use a single-active giga block-sync subscriber; validators cap inbound fullnode block-sync peers (autobahn-max-inbound-fullnode-peers, default 10) and serve block-sync-only RPC to non-committee peers. LastCommittedBlockNumber now tracks app-executed height on both paths.

Config & ops: autobahn-config-file moved to top-level TOML (with tests); RPC init waits for genesis and can gen Autobahn config when AUTOBAHN=true. Docker/Makefile use DOCKER_PLATFORM and pass AUTOBAHN / CLUSTER_SIZE to the rpc node.

CI & tests: New autobahn-integration-tests workflow job (make autobahn-integration-test); integration suite boots an rpc fullnode sidecar, polls height via CometBFT RPC (26669), and exercises halt/liveness/recovery and EVM forwarding.

Reviewed by Cursor Bugbot for commit b8e9d21. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 30, 2026

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJun 3, 2026, 12:52 PM

@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

❌ Patch coverage is 51.71103% with 127 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.23%. Comparing base (2151ba0) to head (b8e9d21).

Files with missing lines Patch % Lines
sei-tendermint/node/setup.go 40.54% 44 Missing ⚠️
sei-tendermint/internal/p2p/giga_router.go 70.68% 21 Missing and 13 partials ⚠️
...ei-tendermint/internal/p2p/giga_router_fullnode.go 9.09% 30 Missing ⚠️
sei-tendermint/internal/p2p/giga/service.go 40.00% 12 Missing ⚠️
sei-tendermint/internal/p2p/giga/data.go 62.50% 0 Missing and 3 partials ⚠️
sei-tendermint/internal/p2p/router.go 66.66% 1 Missing and 1 partial ⚠️
sei-tendermint/node/node.go 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            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     
Flag Coverage Δ
sei-chain-pr 61.82% <51.71%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-tendermint/config/config.go 73.17% <ø> (ø)
sei-tendermint/config/toml.go 55.00% <ø> (ø)
sei-tendermint/internal/p2p/routeroptions.go 81.25% <ø> (ø)
sei-tendermint/internal/rpc/core/env.go 76.15% <100.00%> (ø)
sei-tendermint/internal/p2p/router.go 90.03% <66.66%> (-0.04%) ⬇️
sei-tendermint/node/node.go 65.46% <50.00%> (+0.08%) ⬆️
sei-tendermint/internal/p2p/giga/data.go 68.53% <62.50%> (ø)
sei-tendermint/internal/p2p/giga/service.go 74.46% <40.00%> (-25.54%) ⬇️
...ei-tendermint/internal/p2p/giga_router_fullnode.go 9.09% <9.09%> (ø)
sei-tendermint/internal/p2p/giga_router.go 67.30% <70.68%> (-2.46%) ⬇️
... and 1 more

... and 101 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@wen-coding wen-coding changed the title feat(autobahn): rpc-only mode forwards eth_sendRawTransaction (CON-309) Implement tx write in Autobahn rpc-only node (CON-309) May 30, 2026
Comment thread app/app.go Outdated
Comment thread app/app.go Outdated
Comment thread sei-tendermint/internal/p2p/giga_router_test.go Outdated
wen-coding and others added 2 commits May 29, 2026 18:14
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>
Comment thread sei-tendermint/node/setup.go
wen-coding and others added 2 commits May 29, 2026 19:57
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>
Comment thread sei-tendermint/internal/p2p/giga_router.go Outdated
wen-coding and others added 2 commits May 29, 2026 20:42
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>
Comment thread sei-tendermint/node/setup.go
wen-coding and others added 5 commits May 29, 2026 21:04
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>
Comment thread app/app.go Outdated
httpServerStartSignalSent bool
wsServerStartSignalSent bool
httpServerStartSignal chan struct{}
wsServerStartSignal chan struct{}
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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We don't need to touch app.go now we are doing read side.

Comment thread sei-tendermint/config/config.go Outdated
// eth_sendRawTransaction to the shard
// owner. No consensus participation.
// Ignored when AutobahnConfigFile is empty.
AutobahnRole string `mapstructure:"autobahn-role"`
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.

nit: shouldn't this be conditioned on the presence of the validator key instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

// 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{
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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

// FinalizeBlock responses are not stored on disk) without reaching into
// the unexported router.cfg.
func (r *GigaRouter) MaxGasPerBlock() int64 {
if r.cfg.RPCOnly {
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.

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

"return nil" would be totally fine. There is no obligation for Run to block if it has nothing to do.

@pompon0
Copy link
Copy Markdown
Contributor

pompon0 commented Jun 1, 2026

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>
wen-coding and others added 2 commits June 1, 2026 22:25
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
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.

Option

// 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 {
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.

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

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

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

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

return error?

key NodeSecretKey
data *data.State
service *giga.Service
poolOut *giga.Pool[NodePublicKey, rpc.Client[giga.API]]
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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

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{}
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.

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

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

how about we have separate constructors and separate config types? I suspect it may make the code less cluttered.

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.

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) {
Copy link
Copy Markdown
Contributor

@pompon0 pompon0 Jun 2, 2026

Choose a reason for hiding this comment

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

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

make Key optional in handshake.

return fmt.Errorf("peer not whitelisted")
if !isCommittee {
select {
case r.inboundRPCOnlyPermits <- struct{}{}:
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.

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

shouldn't this be common?

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

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

do we really need those non-nil checks?

wen-coding and others added 4 commits June 2, 2026 12:40
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>
@wen-coding wen-coding changed the title Add Autobahn rpc-only node (CON-309) Add Autobahn fullnode (CON-309) Jun 2, 2026
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>
Comment thread sei-tendermint/internal/p2p/giga_router_fullnode.go Outdated
Comment thread sei-tendermint/internal/p2p/giga_router_fullnode.go
wen-coding and others added 2 commits June 2, 2026 21:59
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>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bf1f20f. Configure here.

Comment thread sei-tendermint/node/node.go
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants