Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions inprocess/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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())
Expand Down
28 changes: 19 additions & 9 deletions inprocess/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <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
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion inprocess/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down
126 changes: 98 additions & 28 deletions integration_test/runner/runner_inprocess_test.go
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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 {
Expand All @@ -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 "<pass>\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")
}
Loading