diff --git a/inprocess/genesis.go b/inprocess/genesis.go index bdd77ad7a2..b8e5188ece 100644 --- a/inprocess/genesis.go +++ b/inprocess/genesis.go @@ -45,9 +45,19 @@ type genesisBuilder struct { balances []banktypes.Balance } -// fundValidator stores a validator operator key in kb, funds its genesis account -// + balances, and writes its self-delegation gentx to gentxsDir keyed by moniker. -// It returns the operator address for downstream client wiring. +// operatorKeyName is the keyring name each node's validator operator key is stored +// under, mirroring the docker localnode convention (`node_admin`). Suites resolve +// the operator by this name — validator address (--bech val), reward/commission +// queries, delegation targets — and run unchanged against the in-process arm. It is +// reserved: each node keyrings its own operator under it, and provisionExtraKeys +// rejects an ExtraKey that reuses the name (which would overwrite the operator with +// a plain account). +const operatorKeyName = "node_admin" + +// fundValidator stores a validator operator key in kb (under operatorKeyName), +// funds its genesis account + balances, and writes its self-delegation gentx to +// gentxsDir keyed by moniker. It returns the operator address for downstream +// client wiring. func (b *genesisBuilder) fundValidator( kb keyring.Keyring, moniker string, @@ -56,9 +66,9 @@ func (b *genesisBuilder) fundValidator( accountTokens, stakingTokens, bondedTokens sdk.Int, p2pHost, p2pPort, nodeID, gentxsDir string, ) (sdk.AccAddress, error) { - addr, _, err := testutil.GenerateSaveCoinKey(kb, moniker, "", true, algo) + addr, _, err := testutil.GenerateSaveCoinKey(kb, operatorKeyName, "", true, algo) if err != nil { - return nil, fmt.Errorf("generate key for %s: %w", moniker, err) + return nil, fmt.Errorf("generate operator key for %s: %w", moniker, err) } balances := sdk.NewCoins( @@ -92,7 +102,7 @@ func (b *genesisBuilder) fundValidator( txb.SetGasLimit(1_000_000) txb.SetMemo(memo) txf := tx.Factory{}.WithChainID(b.chainID).WithMemo(memo).WithKeybase(kb).WithTxConfig(b.txConfig) - if err := tx.Sign(txf, moniker, txb, true); err != nil { + if err := tx.Sign(txf, operatorKeyName, txb, true); err != nil { return nil, err } txBz, err := b.txConfig.TxJSONEncoder()(txb.GetTx()) diff --git a/inprocess/harness.go b/inprocess/harness.go index c581ce02d7..ccb7b8fee2 100644 --- a/inprocess/harness.go +++ b/inprocess/harness.go @@ -79,17 +79,20 @@ type Options struct { // ExtraKeys are non-validator genesis accounts to create + fund. Each key is // written into its target node's home `test` keyring (so a host `seid --home // --keyring-backend test` resolves it) and funded at genesis. This is - // the bridge the YAML runner's in-process arm needs: the bank suite signs as - // `admin` (node 0) and the docker topology also seeds `node_admin` per node. + // the bridge the YAML runner's in-process arm needs — e.g. the bank suite signs + // as `admin` on node 0. It must NOT seed `node_admin`: that name is the per-node + // validator operator the harness provisions (see operatorKeyName), and + // provisionExtraKeys rejects it. ExtraKeys []ExtraKey } -// ExtraKey is a non-validator genesis account the harness creates and funds. It -// mirrors the docker localnode topology where `admin` lives on node 0 only and -// `node_admin` exists per node, so suites that sign as those names run unchanged -// against the in-process arm. +// ExtraKey is a non-validator genesis account the harness creates and funds — a +// suite signing key the docker localnode seeds (e.g. `admin` on node 0), so suites +// that sign as those names run unchanged against the in-process arm. The validator +// operator `node_admin` is NOT an ExtraKey: the harness provisions it per node +// (see operatorKeyName), and provisionExtraKeys rejects it. type ExtraKey struct { - // Name is the keyring key name (e.g. "admin", "node_admin"). + // Name is the keyring key name (e.g. "admin"); must not be operatorKeyName. Name string // Node is the 0-based validator index whose home keyring receives the key. Node int @@ -324,14 +327,21 @@ func (net *Network) provisionNodes(enc encoding, gb *genesisBuilder) error { // `test` keyring and funds its genesis account. It runs after provisionNodes (so // every node's keyring exists) and before genesis assembly (so the balances fold // into the base genesis). This is the keyring/home bridge the YAML runner's -// in-process arm relies on — `admin` on node 0, `node_admin` per node — matching -// the docker localnode topology so bank suites sign unchanged. +// in-process arm relies on — e.g. `admin` on node 0 — matching the docker +// localnode signing keys so suites sign unchanged. (node_admin is not seeded +// here; it is the per-node operator provisioned in provisionNodes.) func (net *Network) provisionExtraKeys(gb *genesisBuilder) error { algoStr := string(hdSecp256k1()) for _, ek := range net.opts.ExtraKeys { if ek.Node < 0 || ek.Node >= len(net.nodes) { return fmt.Errorf("extra key %q targets node %d, out of range [0,%d)", ek.Name, ek.Node, len(net.nodes)) } + // operatorKeyName is provisioned per node (fundValidator); an ExtraKey reusing + // it would overwrite the operator with a plain account — fail-loud rather than + // silently corrupt the operator identity. + if ek.Name == operatorKeyName { + return fmt.Errorf("inprocess: ExtraKey name %q is reserved for the per-node validator operator; use a different name", ek.Name) + } kb := net.nodes[ek.Node].clientCx.Keyring algos, _ := kb.SupportedAlgorithms() algo, err := keyring.NewSigningAlgoFromString(algoStr, algos) diff --git a/inprocess/harness_test.go b/inprocess/harness_test.go index 482452dd3e..4f28d0c077 100644 --- a/inprocess/harness_test.go +++ b/inprocess/harness_test.go @@ -81,7 +81,7 @@ func assertCrossNodeTxRoundTrip(t *testing.T, ctx context.Context, net *Network) if err != nil { t.Fatalf("build tx: %v", err) } - if err := tx.Sign(txf, n0.moniker, txb, true); err != nil { + if err := tx.Sign(txf, operatorKeyName, txb, true); err != nil { t.Fatalf("sign tx: %v", err) } txBz, err := n0.clientCx.TxConfig.TxEncoder()(txb.GetTx()) @@ -118,6 +118,20 @@ func TestStartRejectsZeroValidators(t *testing.T) { } } +// TestStartRejectsOperatorKeyNameExtraKey guards the operator-key reservation: an +// ExtraKey reusing operatorKeyName would overwrite the per-node validator operator +// with a plain account (a silent operator-identity corruption), so Start rejects it +// before bring-up. The reject path returns before the one-network slot is claimed. +func TestStartRejectsOperatorKeyNameExtraKey(t *testing.T) { + _, err := Start(context.Background(), Options{ + Validators: 1, + ExtraKeys: []ExtraKey{{Name: operatorKeyName, Node: 0}}, + }) + if err == nil { + t.Fatalf("Start with an ExtraKey named %q: want error, got nil", operatorKeyName) + } +} + // TestBuildNodeConfigMetricsOff mechanically guards the metrics-off constraint // (see doc.go): a built node config must keep Instrumentation.Prometheus = false. // Re-enabling it reintroduces the process-wide dup-registry panic, so this catches diff --git a/integration_test/runner/runner_inprocess_test.go b/integration_test/runner/runner_inprocess_test.go index 49eb166389..64be83ab8e 100644 --- a/integration_test/runner/runner_inprocess_test.go +++ b/integration_test/runner/runner_inprocess_test.go @@ -1,17 +1,31 @@ //go:build inprocess -// Package runner_test's in-process arm runs the YAML suites against an -// inprocess.Network (no docker). It is tagged `inprocess` so it never enters a -// normal runner build; the docker-backed runner_test.go (tag `yaml_integration`) -// is unaffected. +// Package runner_test's in-process arm runs the YAML suites against a single +// shared inprocess.Network (no docker), started once in TestMain and reused by +// every suite. This mirrors the docker job model — one cluster, suites run +// sequentially against it — and amortizes the ~8s bring-up + one seid build +// across all suites. // -// Run the bank send suite in-memory: +// Suites share one chain + keyring, so a shared signer (admin, node_admin) +// accumulates balances, sequence numbers, and keyring entries across suites in run +// order. Assert deltas or query pinned heights — never an absolute latest-state +// value for a shared signer (the docker job shares state the same way). // -// go test -tags inprocess -run TestInProcessBankModule -v -timeout 600s ./integration_test/runner/ +// A shared network is required, not merely efficient: the harness allows only one +// in-process network per process (the EVM worker pool / metrics printer / +// Prometheus registries are process-global singletons), so a per-suite Start +// would fail the second time. +// +// It is tagged `inprocess` so it never enters a normal runner build; the +// docker-backed runner_test.go (tag `yaml_integration`) is unaffected. +// +// go test -tags inprocess -v -timeout 900s ./integration_test/runner/ package runner_test import ( "context" + "fmt" + "os" "testing" "time" @@ -21,14 +35,14 @@ import ( "github.com/sei-protocol/sei-chain/integration_test/runner" ) -// chainID is the chain-id the bank suite signs with (`--chain-id=sei` in the tx -// helpers). The in-process harness must use the same id, and the per-node -// client.toml it writes carries it so bare `seid` calls match. +// chainID is the chain-id the suites sign with (`--chain-id=sei`). The harness +// uses the same id, and the per-node client.toml it writes carries it so bare +// `seid` calls match. const chainID = "sei" // adminFunding mirrors the docker step2_genesis admin grant -// (1000000000000000000000usei). Large enough to cover the suite's sends + fees -// with room to spare. +// (1000000000000000000000usei) — large enough to cover every suite's sends + fees +// across the shared network's lifetime. func adminFunding() sdk.Coins { amt, ok := sdk.NewIntFromString("1000000000000000000000") if !ok { @@ -37,38 +51,94 @@ func adminFunding() sdk.Coins { return sdk.NewCoins(sdk.NewCoin("usei", amt)) } -// TestInProcessBankModule runs bank_module/send_funds_test.yaml end-to-end -// through the runner's in-process arm: a genesis-funded `admin` on node 0 -// (the suite's signing key) drives a real bank tx + historical balance -// queries, in-memory, no docker. -func TestInProcessBankModule(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute) +// sharedNet is the one in-process network every suite runs against. TestMain owns +// its lifecycle; it is non-nil for the duration of m.Run(). Suites must run +// sequentially — the Network (and the per-node keyring the suites mutate) is not +// goroutine-safe, so no TestInProcess* suite may call t.Parallel(). +var sharedNet *inprocess.Network + +func TestMain(m *testing.M) { + os.Exit(runSuites(m)) +} + +// runSuites brings up the shared network, runs the suites, and tears it down. It +// is split from TestMain so the deferred Close/cancel run before os.Exit (which +// skips defers). +func runSuites(m *testing.M) int { + // go test's own -timeout is the authoritative run bound (a hang panics there, + // skipping defers); this generous ctx is the backstop for -timeout 0 and the + // parent for the nodes, so it must outlive m.Run(). + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) defer cancel() net, err := inprocess.Start(ctx, inprocess.Options{ - // Three validators — the smallest real multi-node topology. send_funds - // asserts only node-0 state, but a single-node run wouldn't exercise - // cross-peer consensus. N is constrained to 1 or >=3 (never 2) by CometBFT's - // block-sync handoff; see inprocess.Options.Validators and the package doc. + // Three validators — the smallest real multi-node topology (N is 1 or >=3; + // see inprocess.Options.Validators). admin is genesis-funded on node 0 (the + // suites' signing key); each node's validator operator is seeded as + // node_admin by the harness, which the distribution/staking suites resolve. Validators: 3, ChainID: chainID, TimeoutCommit: time.Second, ExtraKeys: []inprocess.ExtraKey{ - // admin lives on node 0 only and is genesis-funded — the docker - // localnode topology the suite signs against. {Name: "admin", Node: 0, Coins: adminFunding()}, }, }) if err != nil { - t.Fatalf("inprocess.Start: %v", err) + fmt.Fprintf(os.Stderr, "inprocess.Start: %v\n", err) + return 1 } defer net.Close() if err := net.WaitReady(ctx); err != nil { - t.Fatalf("WaitReady: %v", err) + fmt.Fprintf(os.Stderr, "WaitReady: %v\n", err) + return 1 } + sharedNet = net + return m.Run() +} + +// TestInProcessBankModule runs the bank send suite against the shared network: a +// genesis-funded `admin` on node 0 drives a real bank tx + historical balance +// queries, in-memory, no docker. +func TestInProcessBankModule(t *testing.T) { + runner.RunFile(t, "../bank_module/send_funds_test.yaml", runner.WithInProcessNetwork(sharedNet)) +} + +// TestInProcessDistributionModule funds the community pool (admin) and withdraws +// validator rewards (node_admin). The latter exercises the operator-key seeding: +// node_admin self-bonds at genesis, so it accrues rewards and resolves a valoper, +// exactly as the docker localnode provides. +func TestInProcessDistributionModule(t *testing.T) { + runner.RunFile(t, "../distribution_module/community_pool.yaml", runner.WithInProcessNetwork(sharedNet)) + runner.RunFile(t, "../distribution_module/rewards.yaml", runner.WithInProcessNetwork(sharedNet)) +} + +// TestInProcessMintModule is skipped in-process: mint_test.yaml asserts values +// fixed by the docker localnode's genesis — both the mint-release schedule and the +// total supply (the supply assertion folds in the full genesis allocation, not +// just minted tokens) — but the harness boots from ModuleBasics.DefaultGenesis +// with its own supply + mint params. Enabling it needs the localnode's genesis +// (schedule AND supply allocation) replicated in the harness genesisBuilder, not +// the mint schedule alone; the exact expected values live in mint_test.yaml. +func TestInProcessMintModule(t *testing.T) { + t.Skip("mint needs the docker localnode genesis (mint schedule + supply allocation) replicated in-process") +} + +// TestInProcessStakingModule runs the staking suite: admin delegates, redelegates, +// and unbonds across validators, each validator's operator address (valoper) +// resolved from node_admin (--bech val). It is cross-node — the YAML's `node:` +// field selects which node's keyring supplies that address — and the operator-key +// seeding is what makes it resolvable on each node. +func TestInProcessStakingModule(t *testing.T) { + runner.RunFile(t, "../staking_module/staking_test.yaml", runner.WithInProcessNetwork(sharedNet)) +} - runner.RunFile(t, "../bank_module/send_funds_test.yaml", - runner.WithInProcessNetwork(net), - ) +// TestInProcessAuthzModule is skipped in-process: the staking/generic YAMLs +// re-`keys add grantee` (a name the send suite already created) and feed +// `printf "\ny\n"` to answer docker's passphrase-then-overwrite prompts. The +// harness's `test` keyring takes no passphrase, so the first line is consumed as +// the overwrite answer and the add aborts. Enabling authz needs keyring-backend +// parity or per-suite key isolation. +func TestInProcessAuthzModule(t *testing.T) { + t.Skip("authz needs keyring-backend parity or per-suite key isolation") }