From 5c32129464b3dd813c64e15b237c050ad80f1671 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 6 Jul 2026 21:44:54 -0700 Subject: [PATCH 1/7] test(inprocess): isolate keyring per RunFile; migrate authz suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add WithIsolatedKeyring(): before a RunFile, the in-process arm clones the target node's test keyring + config dir into a temp overlay and points the seid shim's --home at it, discarding it in t.Cleanup. A fresh overlay means `keys add grantee` never pre-exists, so the docker suites' `printf "\ny\n"` override prompt is harmlessly ignored and the add succeeds — no YAML edits, no keyring-backend change. Isolation is keyring-namespace only (not on-chain state), which is exactly what the authz suites need and mirrors docker's per-container keyrings. Flip the three authz suites (send/staking/generic) from skip to real in-process wrappers on the shared network. Full in-process package green (8 suites, ~188s). The capability rides an optional execer interface (keyringIsolator), mirroring the existing backendPreparer seam; the docker arm ignores it. Co-Authored-By: Claude Opus 4.8 --- integration_test/runner/runner.go | 28 +++++++++++ integration_test/runner/runner_inprocess.go | 46 ++++++++++++++++++- .../runner/runner_inprocess_test.go | 20 +++++--- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/integration_test/runner/runner.go b/integration_test/runner/runner.go index d5b2e5cfb2..a81d9808e9 100644 --- a/integration_test/runner/runner.go +++ b/integration_test/runner/runner.go @@ -70,6 +70,14 @@ type backendPreparer interface { prepare(t *testing.T) error } +// keyringIsolator is an optional execer capability: an arm that can give a RunFile a +// private keyring implements it, and RunFile invokes it on the parent test (so the +// overlay outlives the per-case subtests) when Options.IsolateKeyring is set. The +// docker arm (per-container keyrings) does not implement it. +type keyringIsolator interface { + isolateKeyring(t *testing.T) error +} + // Options controls how RunFile executes commands. type Options struct { // DefaultContainer is the docker container used when an Input has no Node set. @@ -84,6 +92,15 @@ type Options struct { // WithInProcessNetwork (build-tagged `inprocess`); it never enters a normal // runner build. exec execer + + // IsolateKeyring, when true, gives this RunFile a private clone of the target + // node's `test` keyring so a suite that `keys add`s a name (e.g. authz's grantee) + // doesn't collide on that name with a sibling suite on the shared network. It + // isolates the keyring namespace, not on-chain state — a suite adding a fixed key + // via `--recover` would still land the same address, so on-chain effects still + // share the chain. Backend-specific: the in-process arm honors it; the docker arm + // (per-container keyrings) ignores it. + IsolateKeyring bool } // Option is a functional option for Options. @@ -111,6 +128,12 @@ func withExecer(e execer) Option { return func(o *Options) { o.exec = e } } +// WithIsolatedKeyring gives this RunFile a private keyring clone (see +// Options.IsolateKeyring). +func WithIsolatedKeyring() Option { + return func(o *Options) { o.IsolateKeyring = true } +} + func newOptions(opts []Option) Options { var o Options //applying default options @@ -136,6 +159,11 @@ func RunFile(t *testing.T, path string, opts ...Option) { if p, ok := o.exec.(backendPreparer); ok { require.NoError(t, p.prepare(t), "prepare backend") } + if o.IsolateKeyring { + if iso, ok := o.exec.(keyringIsolator); ok { + require.NoError(t, iso.isolateKeyring(t), "isolate keyring") + } + } data, err := os.ReadFile(path) //nolint:gosec require.NoError(t, err, "read %s: %v", path, err) var cases []TestCase diff --git a/integration_test/runner/runner_inprocess.go b/integration_test/runner/runner_inprocess.go index 2e05b62ce0..7d9a4c6560 100644 --- a/integration_test/runner/runner_inprocess.go +++ b/integration_test/runner/runner_inprocess.go @@ -45,6 +45,12 @@ type inProcessExecer struct { once sync.Once binDir string // dir holding the seid shim + real binary, prepended to PATH setup error // first-build error, returned to every run after + + // overlayHomes maps a node's real home to a per-RunFile keyring-isolated clone, + // populated by isolateKeyring when Options.IsolateKeyring is set (nil otherwise ⇒ + // commands use the real home). Written once, before any case runs; read by run — + // no concurrent access, since cases run sequentially. + overlayHomes map[string]string } func newInProcessExecer(net *inprocess.Network) *inProcessExecer { @@ -75,10 +81,16 @@ func (e *inProcessExecer) run(t *testing.T, cmd, node string, envMap map[string] return "", err } c.Dir = root + // A keyring-isolated RunFile points the shim's --home at a per-node overlay clone + // (see isolateKeyring); otherwise the real node home. + home := h.Home() + if ov, ok := e.overlayHomes[home]; ok { + home = ov + } c.Env = append(os.Environ(), envMapSlice(envMap)...) c.Env = append(c.Env, "PATH="+e.binDir+string(os.PathListSeparator)+os.Getenv("PATH"), - "SEID_HOME="+h.Home(), + "SEID_HOME="+home, // SEID_NODE makes TM RPC targeting explicit via the shim's --node flag // rather than resting solely on the per-node client.toml. RPCNodeAddr is the // tcp:// form --node wants. @@ -132,6 +144,38 @@ func (e *inProcessExecer) prepare(t *testing.T) error { return e.ensureBin(t) } +// isolateKeyring is the keyringIsolator hook: it clones each node's `test` keyring + +// client.toml into a temp overlay home, so a suite that `keys add`s a name (authz's +// grantee) can't collide with a sibling suite or a prior run on the shared keyring. +// run then points the seid shim's --home at the overlay (see overlayHomes); the +// running node keeps its real home. Cloning admin + node_admin (whose privkeys match +// genesis) keeps signing working; only new adds are sandboxed. Registered on the +// parent test so the overlays outlive the per-case subtests. +func (e *inProcessExecer) isolateKeyring(t *testing.T) error { + t.Helper() + e.overlayHomes = make(map[string]string, e.net.Len()) + for i := 0; i < e.net.Len(); i++ { + h := e.net.Node(i) + overlay, err := os.MkdirTemp("", "sei-keyring-overlay-") + if err != nil { + return err + } + t.Cleanup(func() { _ = os.RemoveAll(overlay) }) + // Clone keyring-test/ (the `test`-backend keys) + the whole config/ dir, so + // `seid --home ` has everything the client path reads (client.toml's + // keyring-backend + chain-id, plus config.toml/app.toml) and regenerates + // nothing — the real home is never touched. + if err := os.CopyFS(filepath.Join(overlay, "keyring-test"), os.DirFS(filepath.Join(h.Home(), "keyring-test"))); err != nil { + return fmt.Errorf("clone keyring for %s: %w", h.Name(), err) + } + if err := os.CopyFS(filepath.Join(overlay, "config"), os.DirFS(filepath.Join(h.Home(), "config"))); err != nil { + return fmt.Errorf("clone config for %s: %w", h.Name(), err) + } + e.overlayHomes[h.Home()] = overlay + } + return nil +} + // ensureBin builds the seid binary once and writes a `seid` shim alongside it, // in a dir prepended to PATH. The shim redirects bare `seid` calls (inside // opaque sourced helpers) to the per-command node home + RPC without rewriting diff --git a/integration_test/runner/runner_inprocess_test.go b/integration_test/runner/runner_inprocess_test.go index ea35effcda..cbbba9a4f5 100644 --- a/integration_test/runner/runner_inprocess_test.go +++ b/integration_test/runner/runner_inprocess_test.go @@ -201,12 +201,18 @@ func TestInProcessFlatKVEvmModule(t *testing.T) { t.Skip("seidb flatkv_evm asserts a docker fixture (pre-deployed EVM contract + recorded balances/heights)") } -// 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. +// TestInProcessAuthzModule runs the three authz suites, each with an isolated +// keyring (WithIsolatedKeyring). Each suite `keys add grantee` under docker's +// `printf "\ny\n"`; on the shared `test` keyring the second suite's re-add of +// an existing `grantee` would hit the override prompt and abort. A per-suite keyring +// overlay makes `grantee` fresh each time, so the add succeeds and the piped input +// is harmlessly ignored — no YAML edit, no keyring-backend change. func TestInProcessAuthzModule(t *testing.T) { - t.Skip("authz needs keyring-backend parity or per-suite key isolation") + for _, f := range []string{ + "../authz_module/send_authorization_test.yaml", + "../authz_module/staking_authorization_test.yaml", + "../authz_module/generic_authorization_test.yaml", + } { + runner.RunFile(t, f, runner.WithInProcessNetwork(sharedNet), runner.WithIsolatedKeyring()) + } } From e688bad9ad363c2452259a6422a28ebd06525acf Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 6 Jul 2026 22:39:07 -0700 Subject: [PATCH 2/7] test(inprocess): add WithSetupScript fixture bring-up; migrate wasm suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add WithSetupScript(path): the in-process arm runs a suite's docker fixture script once through the seid shim (at the repo root, so bare `seid` lands on the target node) before the cases, fail-loud on non-zero exit. Two env-defaults on the fixture scripts (SEIDBIN, FIXTURE_SIGNER) let a non-docker caller repoint the binary + signer while leaving docker byte-identical when unset. Fixtures write contract addresses into the repo tree; snapshotContractsTxt keeps that side-effect-free — git-tracked *.txt are restored only if changed, untracked outputs (and any prior hard-killed run's leftovers) are removed, so a run leaves no diff and self-heals stale artifacts. Add InProcessSuite: runs the one-time setup (build → keyring overlay → fixture) once, then reuses it across RunFiles — the wasm suites need one gringotts deploy + one keyring shared by several files, which a per-file RunFile would rebuild. Migrate the timelocked-token (gringotts) wasm suites: TestInProcessWasmModuleCore (delegation → admin → withdraw) + TestInProcessWasmModuleEmergencyWithdraw, each its own fresh deploy + isolated keyring, mirroring docker's two fixture deploys. Both green (~40s + ~33s, ~3x vs the 4-min docker job). Also force the kv tx-index sink on in the harness so the suites' `-b block` resolves (BroadcastTxCommit polls the index, not the event bus). Co-Authored-By: Claude Opus 4.8 --- inprocess/harness.go | 3 + .../deploy_timelocked_token_contract.sh | 6 +- integration_test/runner/runner.go | 50 ++++- integration_test/runner/runner_inprocess.go | 184 ++++++++++++++++-- .../runner/runner_inprocess_test.go | 34 +++- 5 files changed, 249 insertions(+), 28 deletions(-) diff --git a/inprocess/harness.go b/inprocess/harness.go index ccb7b8fee2..4e85253980 100644 --- a/inprocess/harness.go +++ b/inprocess/harness.go @@ -451,6 +451,9 @@ func buildNodeConfig(nodeDir, moniker string, timeoutCommit time.Duration) (*con tmCfg.Moniker = moniker tmCfg.SetRoot(nodeDir) tmCfg.Consensus.UnsafeCommitTimeoutOverride = timeoutCommit + // Force the kv tx-index sink on. `-b block` (BroadcastTxCommit) polls the tx + // index, not the event bus, so YAML suites using it (wasm) hang without this — + // a mode-based config would give a validator the null indexer. tmCfg.TxIndex = config.TestTxIndexConfig() // loopback conn-tracker ceiling: loopback collapses every peer onto 127.0.0.1, // so the router's IP-keyed conn-tracker counts all N-1 inbound on one key. diff --git a/integration_test/contracts/deploy_timelocked_token_contract.sh b/integration_test/contracts/deploy_timelocked_token_contract.sh index ff1b38e4f4..e2b769be28 100755 --- a/integration_test/contracts/deploy_timelocked_token_contract.sh +++ b/integration_test/contracts/deploy_timelocked_token_contract.sh @@ -1,7 +1,9 @@ #!/bin/bash -seidbin=$(which ~/go/bin/seid | tr -d '"') -keyname=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"') +# SEIDBIN / FIXTURE_SIGNER let a non-docker caller repoint the binary + signer +# without changing docker's behavior (both unset → the original computed values). +seidbin=${SEIDBIN:-$(which ~/go/bin/seid | tr -d '"')} +keyname=${FIXTURE_SIGNER:-$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"')} chainid=$($seidbin status | jq ".NodeInfo.network" | tr -d '"') seihome=$(git rev-parse --show-toplevel | tr -d '"') migration=$1 diff --git a/integration_test/runner/runner.go b/integration_test/runner/runner.go index a81d9808e9..e757f26a99 100644 --- a/integration_test/runner/runner.go +++ b/integration_test/runner/runner.go @@ -78,6 +78,16 @@ type keyringIsolator interface { isolateKeyring(t *testing.T) error } +// setupRunner is an optional execer capability: a backend that can run a suite's +// fixture bring-up script (deploy contracts, seed keys the cases assume) once +// implements it, and RunFile invokes it after keyring isolation and before any +// case when Options.SetupScript is set. The in-process arm runs the script +// through its seid shim; the docker arm (fixtures brought up by the CI harness) +// does not implement it. +type setupRunner interface { + runSetup(t *testing.T, scriptPath string, opts Options) error +} + // Options controls how RunFile executes commands. type Options struct { // DefaultContainer is the docker container used when an Input has no Node set. @@ -101,6 +111,13 @@ type Options struct { // share the chain. Backend-specific: the in-process arm honors it; the docker arm // (per-container keyrings) ignores it. IsolateKeyring bool + + // SetupScript, when set, is a repo-root-relative fixture bring-up script run + // once before the cases (deploy the contracts + seed the keys the suites + // assume). Backend-specific: the in-process arm runs it through its shim + // (executed at the repo root, so the path is repo-root-relative); the docker + // arm ignores it (its fixtures are brought up by the CI harness). + SetupScript string } // Option is a functional option for Options. @@ -134,6 +151,12 @@ func WithIsolatedKeyring() Option { return func(o *Options) { o.IsolateKeyring = true } } +// WithSetupScript runs a fixture bring-up script once before the cases (see +// Options.SetupScript). The path is repo-root-relative. +func WithSetupScript(path string) Option { + return func(o *Options) { o.SetupScript = path } +} + func newOptions(opts []Option) Options { var o Options //applying default options @@ -153,9 +176,19 @@ func newOptions(opts []Option) Options { func RunFile(t *testing.T, path string, opts ...Option) { t.Helper() o := newOptions(opts) - // One-time backend setup, scoped to the parent test so it runs before — and - // outlives — every per-case subtest (see ensureBin). The docker arm implements - // nothing here. + runSuiteSetup(t, o) + runCases(t, path, o) +} + +// runSuiteSetup runs the one-time, parent-scoped backend hooks in order — the +// backend build, keyring isolation, then the fixture script — so the fixture's +// `keys add` and contract deploys land in the isolated keyring. It is scoped to +// the parent test so it runs before, and outlives, every per-case subtest (see +// ensureBin). RunFile runs it per file; a caller that shares one setup across +// several files (the in-process suite) runs it once. The docker arm implements +// none of these hooks. +func runSuiteSetup(t *testing.T, o Options) { + t.Helper() if p, ok := o.exec.(backendPreparer); ok { require.NoError(t, p.prepare(t), "prepare backend") } @@ -164,6 +197,17 @@ func RunFile(t *testing.T, path string, opts ...Option) { require.NoError(t, iso.isolateKeyring(t), "isolate keyring") } } + if o.SetupScript != "" { + if sr, ok := o.exec.(setupRunner); ok { + require.NoError(t, sr.runSetup(t, o.SetupScript, o), "run setup script") + } + } +} + +// runCases parses path as a YAML list of TestCases and runs each as a subtest of +// t; it assumes the one-time setup (runSuiteSetup) already ran. +func runCases(t *testing.T, path string, o Options) { + t.Helper() data, err := os.ReadFile(path) //nolint:gosec require.NoError(t, err, "read %s: %v", path, err) var cases []TestCase diff --git a/integration_test/runner/runner_inprocess.go b/integration_test/runner/runner_inprocess.go index 7d9a4c6560..bda6fdd076 100644 --- a/integration_test/runner/runner_inprocess.go +++ b/integration_test/runner/runner_inprocess.go @@ -7,6 +7,7 @@ package runner import ( + "bytes" "errors" "fmt" "os" @@ -33,6 +34,39 @@ func WithInProcessNetwork(net *inprocess.Network) Option { return withExecer(newInProcessExecer(net)) } +// InProcessSuite runs several YAML files against one shared network with a single +// one-time setup (build → optional keyring overlay → optional fixture script), +// then reuses it for every RunFile. Use it when a group of files shares fixture +// state a per-file RunFile would rebuild — the wasm suites read one gringotts +// deploy + one keyring. A plain RunFile suffices when files are independent (e.g. +// authz, which wants a fresh overlay per file). +type InProcessSuite struct { + t *testing.T + opts Options +} + +// NewInProcessSuite binds net, runs the one-time setup once (see runSuiteSetup for +// hook ordering), and returns a suite whose RunFile reuses it. Pass the setup +// options (WithSetupScript, WithIsolatedKeyring); the network is bound here, so +// WithInProcessNetwork is not needed. Setup and every RunFile run on t, so the +// keyring overlay and seid binary outlive all subtests. +func NewInProcessSuite(t *testing.T, net *inprocess.Network, opts ...Option) *InProcessSuite { + t.Helper() + e := newInProcessExecer(net) + // Bind the execer last so it wins over any stray WithInProcessNetwork. + o := newOptions(append(append([]Option{}, opts...), withExecer(e))) + runSuiteSetup(t, o) + return &InProcessSuite{t: t, opts: o} +} + +// RunFile runs one YAML file against the suite's shared setup. Unlike the +// package-level RunFile it does not re-run setup, and it uses the suite's own test +// so the cases stay within the setup's lifetime. +func (s *InProcessSuite) RunFile(path string) { + s.t.Helper() + runCases(s.t, path, s.opts) +} + // inProcessExecer runs commands on the host against an inprocess.Network. It // shims `seid` so opaque sourced helper scripts (which call bare `seid` / // `$seidbin`) land on the right node: the shim prepends `--home "$SEID_HOME"` @@ -67,22 +101,44 @@ func (e *inProcessExecer) run(t *testing.T, cmd, node string, envMap map[string] if err := e.ensureBin(t); err != nil { return "", fmt.Errorf("prepare seid: %w", err) } - h, err := e.nodeFor(node) + c, err := e.command(t, cmd, node, envMap, opts) if err != nil { return "", err } + out, err := c.Output() + stdout := strings.TrimSpace(string(out)) + if err != nil { + var exit *exec.ExitError + if errors.As(err, &exit) { + t.Logf(" (exit %d) stderr: %s", exit.ExitCode(), strings.TrimSpace(string(exit.Stderr))) + return stdout, nil + } + return stdout, err + } + return stdout, nil +} +// command builds the host exec.Cmd for cmd targeted at node: the seid shim on +// PATH, the per-node targeting env (SEID_HOME → the overlay clone when the +// RunFile is keyring-isolated, else the real home; SEID_NODE; the EVM +// endpoints), the accumulated capture env, and CWD at the repo root. Shared by +// run (which swallows non-zero exit per the docker contract) and runSetup (which +// treats it as fatal). Callers ensure the binary is built (ensureBin) first. +func (e *inProcessExecer) command(t *testing.T, cmd, node string, envMap map[string]string, opts Options) (*exec.Cmd, error) { + t.Helper() + h, err := e.nodeFor(node) + if err != nil { + return nil, err + } c := exec.Command(opts.Shell, "-c", cmd) //nolint:gosec // Run from the repo root so the suites' relative `source // integration_test/utils/_tx_helpers.sh` resolves (docker runs with the repo // mounted at the container CWD; `go test` runs with CWD = the package dir). root, err := repoRoot() if err != nil { - return "", err + return nil, err } c.Dir = root - // A keyring-isolated RunFile points the shim's --home at a per-node overlay clone - // (see isolateKeyring); otherwise the real node home. home := h.Home() if ov, ok := e.overlayHomes[home]; ok { home = ov @@ -100,18 +156,7 @@ func (e *inProcessExecer) run(t *testing.T, cmd, node string, envMap map[string] // Some EVM suites read EVM_RPC; keep parity with SEI_EVM_RPC. "EVM_RPC="+h.EVMRPC(), ) - - out, err := c.Output() - stdout := strings.TrimSpace(string(out)) - if err != nil { - var exit *exec.ExitError - if errors.As(err, &exit) { - t.Logf(" (exit %d) stderr: %s", exit.ExitCode(), strings.TrimSpace(string(exit.Stderr))) - return stdout, nil - } - return stdout, err - } - return stdout, nil + return c, nil } // nodeFor maps a "sei-node-N" moniker (the docker container naming the suites @@ -176,6 +221,113 @@ func (e *inProcessExecer) isolateKeyring(t *testing.T) error { return nil } +// runSetup is the setupRunner hook: it runs a suite's fixture script once through +// the same shimmed environment the cases use (bare `seid` in the script lands on +// the target node; CWD at the repo root), before any case. It sets SEIDBIN=seid +// (the shim) and FIXTURE_SIGNER=admin (the genesis-funded key docker's +// keys-list[0] resolves to) — the two env-defaults the fixture scripts read. +// Unlike run, a non-zero script exit is fatal: a failed fixture must fail the +// suite, not silently leave the cases to assert against missing state. +func (e *inProcessExecer) runSetup(t *testing.T, scriptPath string, opts Options) error { + t.Helper() + if err := e.ensureBin(t); err != nil { + return fmt.Errorf("prepare seid: %w", err) + } + // Fixtures write *-contract-addr.txt (and seidb height records) into the repo + // tree; snapshot first so t.Cleanup restores it to a clean worktree. + if err := e.snapshotContractsTxt(t); err != nil { + return err + } + // node "" → node 0 (admin's home), the suites' default signing home. + c, err := e.command(t, "bash "+scriptPath, "", map[string]string{ + "SEIDBIN": "seid", + "FIXTURE_SIGNER": "admin", + }, opts) + if err != nil { + return err + } + if out, err := c.CombinedOutput(); err != nil { + return fmt.Errorf("setup script %s: %w\n%s", scriptPath, err, out) + } + return nil +} + +// snapshotContractsTxt keeps the fixtures dir's *.txt free of in-process side +// effects. Only git-tracked *.txt are content-saved, and restored only if the +// fixture changed them (so untouched records like the seidb heights aren't +// rewritten). Any untracked *.txt present at cleanup — the fixture's own address +// outputs, or a prior hard-killed run's leftovers (t.Cleanup is skipped on +// -timeout; see TestMain) — is removed, which also self-heals such leftovers on +// the next run. Restore errors are surfaced, not swallowed. +func (e *inProcessExecer) snapshotContractsTxt(t *testing.T) error { + t.Helper() + root, err := repoRoot() + if err != nil { + return err + } + const rel = "integration_test/contracts" + dir := filepath.Join(root, rel) + tracked, err := trackedTxt(root, rel) + if err != nil { + return err + } + saved := make(map[string][]byte, len(tracked)) + for _, p := range tracked { + b, err := os.ReadFile(p) //nolint:gosec + if err != nil { + return fmt.Errorf("snapshot %s: %w", p, err) + } + saved[p] = b + } + t.Cleanup(func() { + var errs []error + now, _ := filepath.Glob(filepath.Join(dir, "*.txt")) + for _, p := range now { + orig, isTracked := saved[p] + if !isTracked { + if err := os.Remove(p); err != nil { + errs = append(errs, err) + } + continue + } + if cur, err := os.ReadFile(p); err != nil || !bytes.Equal(cur, orig) { + if err := os.WriteFile(p, orig, 0o644); err != nil { //nolint:gosec + errs = append(errs, err) + } + } + } + for p, orig := range saved { + if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) { + if err := os.WriteFile(p, orig, 0o644); err != nil { //nolint:gosec + errs = append(errs, err) + } + } + } + if len(errs) > 0 { + t.Errorf("restore %s/*.txt: %v", rel, errs) + } + }) + return nil +} + +// trackedTxt returns absolute paths of the git-tracked *.txt files under the +// repo-relative dir. Untracked files (fixture outputs) are excluded on purpose — +// snapshotContractsTxt removes those rather than preserving them, so git is the +// authority on which files are ours to restore. +func trackedTxt(root, dir string) ([]string, error) { + out, err := exec.Command("git", "-C", root, "ls-files", "--", dir).Output() + if err != nil { + return nil, fmt.Errorf("git ls-files %s: %w", dir, err) + } + var paths []string + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + if strings.HasSuffix(line, ".txt") { + paths = append(paths, filepath.Join(root, line)) + } + } + return paths, nil +} + // ensureBin builds the seid binary once and writes a `seid` shim alongside it, // in a dir prepended to PATH. The shim redirects bare `seid` calls (inside // opaque sourced helpers) to the per-command node home + RPC without rewriting diff --git a/integration_test/runner/runner_inprocess_test.go b/integration_test/runner/runner_inprocess_test.go index cbbba9a4f5..71dc383425 100644 --- a/integration_test/runner/runner_inprocess_test.go +++ b/integration_test/runner/runner_inprocess_test.go @@ -185,13 +185,33 @@ func TestInProcessBankSimulation(t *testing.T) { runner.RunFile(t, "../bank_module/simulation_tx.yaml", runner.WithInProcessNetwork(sharedNet)) } -// TestInProcessWasmModule is skipped in-process: the timelocked-token suites execute -// against a docker-fixture contract — a pre-deployed gringotts instance whose address -// (and the admin1 signer) come from integration_test/contracts/gringotts-contract-addr.txt, -// which the shared harness network doesn't build. Migrating it needs that contract -// deployed + its address seeded in-process. -func TestInProcessWasmModule(t *testing.T) { - t.Skip("wasm timelocked suites assert a docker fixture (pre-deployed gringotts contract + admin1)") +// timelockedFixture is the gringotts bring-up docker runs before each wasm group +// (deploy goblin + gringotts, seed admin1-4/op/etc, record the addresses). +// WithSetupScript runs it in-process through the seid shim; WithIsolatedKeyring +// gives each group its own overlay so the fixture's repeated `keys add admin1` +// doesn't hit the override prompt on the second deploy. +const timelockedFixture = "integration_test/contracts/deploy_timelocked_token_contract.sh" + +// TestInProcessWasmModuleCore runs delegation → admin → withdraw against one fresh +// gringotts deploy, mirroring docker's TestWasmModuleCore. The three share the +// suite's single deploy + keyring; order matters (withdraw depends on prior state). +func TestInProcessWasmModuleCore(t *testing.T) { + s := runner.NewInProcessSuite(t, sharedNet, + runner.WithSetupScript(timelockedFixture), + runner.WithIsolatedKeyring()) + s.RunFile("../wasm_module/timelocked_token_delegation_test.yaml") + s.RunFile("../wasm_module/timelocked_token_admin_test.yaml") + s.RunFile("../wasm_module/timelocked_token_withdraw_test.yaml") +} + +// TestInProcessWasmModuleEmergencyWithdraw needs a pristine gringotts (Core's flows +// mutate the contract), so it deploys its own fixture — the second deploy docker +// runs before TestWasmModuleEmergencyWithdraw. +func TestInProcessWasmModuleEmergencyWithdraw(t *testing.T) { + s := runner.NewInProcessSuite(t, sharedNet, + runner.WithSetupScript(timelockedFixture), + runner.WithIsolatedKeyring()) + s.RunFile("../wasm_module/timelocked_token_emergency_withdraw_test.yaml") } // TestInProcessFlatKVEvmModule is skipped in-process: flatkv_evm_test.yaml asserts a From fc786aa9d2ad574c085934dc976330875ac97c5c Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 6 Jul 2026 22:57:26 -0700 Subject: [PATCH 3/7] test(distribution): de-flake reward-increase assertion with a height barrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REWARDS_AFTER_TX > REWARDS_START was ~35% flaky in-process (2/5 on baseline), failing with the two reads exactly equal. Fees collected in block H are distributed to validators in BeginBlock of H+1, but the suite read rewards after only `sleep 1` — at the harness's 1s commit time that marginally straddles the distribution block, so the query sometimes ran before H+1 committed. Replace the plain send + sleep with `bank_send_and_get_height` (captures the inclusion height H) + `wait_until_height_exceeds $TX_HEIGHT` (blocks until H+1 is committed), then read — deterministic, no sleep. Same pattern bank_module already uses in both the docker and in-process arms; docker (mint-on) was never the failing arm, and the barrier only makes the mint-off harness path deterministic. 10/10 green in-process (was 2/5); full in-process suite now fully green. Co-Authored-By: Claude Opus 4.8 --- integration_test/distribution_module/rewards.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/integration_test/distribution_module/rewards.yaml b/integration_test/distribution_module/rewards.yaml index d93c103dab..4462de28bd 100644 --- a/integration_test/distribution_module/rewards.yaml +++ b/integration_test/distribution_module/rewards.yaml @@ -9,10 +9,12 @@ # Get current rewards - cmd: seid q distribution rewards $NODE_ADMIN_ACC -o json | jq -r "(.total[0].amount // 0) | tonumber" env: REWARDS_START - # Simple tx to increase rewards - - cmd: printf "12345678\n" | seid tx bank send $NODE_ADMIN_ACC $DISTRIBUTION_TEST_ACC 1sei -b block --fees 2000usei --chain-id sei -y - # Wait a couple seconds before querying to reduce likelihood of flaky test results - - cmd: sleep 1 + # Fee-paying tx to increase rewards, capturing its inclusion height. Fees from + # block H are distributed to validators in BeginBlock of H+1, so wait past H + # before re-reading — a fixed sleep races the block time and flakes (equal reads). + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && bank_send_and_get_height node_admin $DISTRIBUTION_TEST_ACC 1sei + env: TX_HEIGHT + - cmd: seidbin=seid; source integration_test/utils/_tx_helpers.sh && wait_until_height_exceeds $TX_HEIGHT # Get rewards after tx - cmd: seid q distribution rewards $NODE_ADMIN_ACC -o json | jq -r "(.total[0].amount // 0) | tonumber" env: REWARDS_AFTER_TX From 317c6901e0a1499e038fb5ab71b6841599d5fa8a Mon Sep 17 00:00:00 2001 From: bdchatham Date: Tue, 7 Jul 2026 08:40:17 -0700 Subject: [PATCH 4/7] test(inprocess): migrate flatkv_evm EVM suite; generalize setup env + cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the first Tier-C EVM suite (flatkv_evm) onto the in-process harness. It runs on the shared network: SeiDB SC+SS (appoptions.go) retain the historical heights the suite queries via `cast --block `, and the fixture adds no keyring names, so no isolation is needed. ~17s vs the multi-minute docker job. - WithSetupEnv(map): fixture-specific env for the setup script. runSetup now passes opts.SetupEnv (the wasm caller moved SEIDBIN/FIXTURE_SIGNER here); command() adds an EVM_RPC_URL alias (the node's dynamic EVM endpoint) so cast-based fixtures + suites resolve it. - flatkv_evm_test.yaml: repoint the 7 hardcoded cast --rpc-url http://localhost:8545 to ${EVM_RPC_URL:-http://localhost:8545} — docker-safe (unset in-container → 8545; in-process → the injected dynamic URL). - Replace the worktree-snapshot helper with cleanFixtureOutputs: a t.Cleanup that runs `git clean -fdX` on integration_test/contracts, removing only git-ignored fixture outputs (the *.txt records) — never a tracked input or a developer's untracked source. This fixes a data-loss risk in the prior generalized snapshot (it removed any untracked file) and drops dead save/restore code (no *.txt under contracts is tracked; all are ignored). The fixture's deploy-receipt .json now writes to /tmp (matching the transfer receipt), so all its contracts/ outputs are ignored. Full in-process suite green (11 suites), clean worktree. Co-Authored-By: Claude Opus 4.8 --- .../contracts/deploy_flatkv_evm_fixture.sh | 2 +- integration_test/runner/runner.go | 11 ++ integration_test/runner/runner_inprocess.go | 108 +++++------------- .../runner/runner_inprocess_test.go | 23 +++- integration_test/seidb/flatkv_evm_test.yaml | 14 +-- 5 files changed, 66 insertions(+), 92 deletions(-) diff --git a/integration_test/contracts/deploy_flatkv_evm_fixture.sh b/integration_test/contracts/deploy_flatkv_evm_fixture.sh index aafcbdf774..09c46e02b4 100755 --- a/integration_test/contracts/deploy_flatkv_evm_fixture.sh +++ b/integration_test/contracts/deploy_flatkv_evm_fixture.sh @@ -207,7 +207,7 @@ if [ -z "$deploy_tx" ]; then fi deploy_receipt=$(wait_for_receipt "$deploy_tx") require_success_receipt "contract deployment" "$deploy_receipt" -printf "%s\n" "$deploy_receipt" > "$out_dir/flatkv_evm_deploy_receipt.json" +printf "%s\n" "$deploy_receipt" > /tmp/flatkv_evm_deploy_receipt.json contract_addr=$(printf "%s\n" "$deploy_receipt" | jq -r '.result.contractAddress // empty') if [ -z "$contract_addr" ] || [ "$contract_addr" = "null" ]; then diff --git a/integration_test/runner/runner.go b/integration_test/runner/runner.go index e757f26a99..3e889f9c91 100644 --- a/integration_test/runner/runner.go +++ b/integration_test/runner/runner.go @@ -118,6 +118,11 @@ type Options struct { // (executed at the repo root, so the path is repo-root-relative); the docker // arm ignores it (its fixtures are brought up by the CI harness). SetupScript string + + // SetupEnv is fixture-specific environment for the SetupScript run (e.g. a + // keyring backend, a signer name, an RPC target). The in-process arm layers it + // under its own node-targeting env (SEID_HOME/SEID_NODE/EVM endpoints). + SetupEnv map[string]string } // Option is a functional option for Options. @@ -157,6 +162,12 @@ func WithSetupScript(path string) Option { return func(o *Options) { o.SetupScript = path } } +// WithSetupEnv sets fixture-specific environment for the SetupScript run (see +// Options.SetupEnv). +func WithSetupEnv(env map[string]string) Option { + return func(o *Options) { o.SetupEnv = env } +} + func newOptions(opts []Option) Options { var o Options //applying default options diff --git a/integration_test/runner/runner_inprocess.go b/integration_test/runner/runner_inprocess.go index bda6fdd076..01cf6852a4 100644 --- a/integration_test/runner/runner_inprocess.go +++ b/integration_test/runner/runner_inprocess.go @@ -7,7 +7,6 @@ package runner import ( - "bytes" "errors" "fmt" "os" @@ -153,8 +152,11 @@ func (e *inProcessExecer) command(t *testing.T, cmd, node string, envMap map[str "SEID_NODE="+h.RPCNodeAddr(), "SEI_EVM_RPC="+h.EVMRPC(), "SEI_EVM_WS="+h.EVMWS(), - // Some EVM suites read EVM_RPC; keep parity with SEI_EVM_RPC. + // EVM_RPC / EVM_RPC_URL are the names EVM suites + cast-based fixtures read; + // alias both to the node's EVM endpoint (dynamic port, so the docker suites' + // hardcoded :8545 must be repointed to these). "EVM_RPC="+h.EVMRPC(), + "EVM_RPC_URL="+h.EVMRPC(), ) return c, nil } @@ -222,27 +224,23 @@ func (e *inProcessExecer) isolateKeyring(t *testing.T) error { } // runSetup is the setupRunner hook: it runs a suite's fixture script once through -// the same shimmed environment the cases use (bare `seid` in the script lands on -// the target node; CWD at the repo root), before any case. It sets SEIDBIN=seid -// (the shim) and FIXTURE_SIGNER=admin (the genesis-funded key docker's -// keys-list[0] resolves to) — the two env-defaults the fixture scripts read. -// Unlike run, a non-zero script exit is fatal: a failed fixture must fail the -// suite, not silently leave the cases to assert against missing state. +// the same shimmed environment the cases use (bare `seid` lands on the target +// node; the node's EVM endpoint in EVM_RPC_URL; CWD at the repo root), before any +// case, with the caller's fixture-specific opts.SetupEnv layered on top. Unlike +// run, a non-zero script exit is fatal: a failed fixture must fail the suite, not +// silently leave the cases to assert against missing state. func (e *inProcessExecer) runSetup(t *testing.T, scriptPath string, opts Options) error { t.Helper() if err := e.ensureBin(t); err != nil { return fmt.Errorf("prepare seid: %w", err) } - // Fixtures write *-contract-addr.txt (and seidb height records) into the repo - // tree; snapshot first so t.Cleanup restores it to a clean worktree. - if err := e.snapshotContractsTxt(t); err != nil { + // Fixtures write outputs into the repo tree; register cleanup first so a clean + // worktree is restored even if the script below fails partway. + if err := e.cleanFixtureOutputs(t); err != nil { return err } // node "" → node 0 (admin's home), the suites' default signing home. - c, err := e.command(t, "bash "+scriptPath, "", map[string]string{ - "SEIDBIN": "seid", - "FIXTURE_SIGNER": "admin", - }, opts) + c, err := e.command(t, "bash "+scriptPath, "", opts.SetupEnv, opts) if err != nil { return err } @@ -252,82 +250,32 @@ func (e *inProcessExecer) runSetup(t *testing.T, scriptPath string, opts Options return nil } -// snapshotContractsTxt keeps the fixtures dir's *.txt free of in-process side -// effects. Only git-tracked *.txt are content-saved, and restored only if the -// fixture changed them (so untouched records like the seidb heights aren't -// rewritten). Any untracked *.txt present at cleanup — the fixture's own address -// outputs, or a prior hard-killed run's leftovers (t.Cleanup is skipped on -// -timeout; see TestMain) — is removed, which also self-heals such leftovers on -// the next run. Restore errors are surfaced, not swallowed. -func (e *inProcessExecer) snapshotContractsTxt(t *testing.T) error { +// cleanFixtureOutputs registers a t.Cleanup that git-cleans the ignored files a +// fixture wrote under integration_test/contracts, so an in-process run leaves no +// worktree diff. `git clean -X` removes only git-ignored files — never a tracked +// input or a developer's untracked source — so it assumes fixtures write only +// ignored outputs there: the wired fixtures (flatkv, timelocked) write *.txt +// (ignored by `integration_test/**/*.txt`) and send other artifacts to /tmp. A +// future fixture emitting a non-ignored file into that dir would be left behind. +// Also self-heals a prior -timeout-killed run's leftovers (t.Cleanup is skipped +// then; see TestMain). +func (e *inProcessExecer) cleanFixtureOutputs(t *testing.T) error { t.Helper() root, err := repoRoot() if err != nil { return err } - const rel = "integration_test/contracts" - dir := filepath.Join(root, rel) - tracked, err := trackedTxt(root, rel) - if err != nil { - return err - } - saved := make(map[string][]byte, len(tracked)) - for _, p := range tracked { - b, err := os.ReadFile(p) //nolint:gosec - if err != nil { - return fmt.Errorf("snapshot %s: %w", p, err) - } - saved[p] = b - } t.Cleanup(func() { - var errs []error - now, _ := filepath.Glob(filepath.Join(dir, "*.txt")) - for _, p := range now { - orig, isTracked := saved[p] - if !isTracked { - if err := os.Remove(p); err != nil { - errs = append(errs, err) - } - continue - } - if cur, err := os.ReadFile(p); err != nil || !bytes.Equal(cur, orig) { - if err := os.WriteFile(p, orig, 0o644); err != nil { //nolint:gosec - errs = append(errs, err) - } - } - } - for p, orig := range saved { - if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) { - if err := os.WriteFile(p, orig, 0o644); err != nil { //nolint:gosec - errs = append(errs, err) - } - } - } - if len(errs) > 0 { - t.Errorf("restore %s/*.txt: %v", rel, errs) + // -X removes only ignored files (never tracked or a dev's untracked source); + // -f is required for clean to act, -d recurses into ignored subdirs. + out, err := exec.Command("git", "-C", root, "clean", "-fdX", "--", "integration_test/contracts").CombinedOutput() //nolint:gosec + if err != nil { + t.Errorf("clean fixture outputs: %v\n%s", err, out) } }) return nil } -// trackedTxt returns absolute paths of the git-tracked *.txt files under the -// repo-relative dir. Untracked files (fixture outputs) are excluded on purpose — -// snapshotContractsTxt removes those rather than preserving them, so git is the -// authority on which files are ours to restore. -func trackedTxt(root, dir string) ([]string, error) { - out, err := exec.Command("git", "-C", root, "ls-files", "--", dir).Output() - if err != nil { - return nil, fmt.Errorf("git ls-files %s: %w", dir, err) - } - var paths []string - for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { - if strings.HasSuffix(line, ".txt") { - paths = append(paths, filepath.Join(root, line)) - } - } - return paths, nil -} - // ensureBin builds the seid binary once and writes a `seid` shim alongside it, // in a dir prepended to PATH. The shim redirects bare `seid` calls (inside // opaque sourced helpers) to the per-command node home + RPC without rewriting diff --git a/integration_test/runner/runner_inprocess_test.go b/integration_test/runner/runner_inprocess_test.go index 71dc383425..9264886093 100644 --- a/integration_test/runner/runner_inprocess_test.go +++ b/integration_test/runner/runner_inprocess_test.go @@ -192,12 +192,17 @@ func TestInProcessBankSimulation(t *testing.T) { // doesn't hit the override prompt on the second deploy. const timelockedFixture = "integration_test/contracts/deploy_timelocked_token_contract.sh" +// timelockedSetupEnv points the fixture at the shimmed seid (SEIDBIN) and the +// genesis-funded signer (FIXTURE_SIGNER); node targeting is added by the arm. +var timelockedSetupEnv = map[string]string{"SEIDBIN": "seid", "FIXTURE_SIGNER": "admin"} + // TestInProcessWasmModuleCore runs delegation → admin → withdraw against one fresh // gringotts deploy, mirroring docker's TestWasmModuleCore. The three share the // suite's single deploy + keyring; order matters (withdraw depends on prior state). func TestInProcessWasmModuleCore(t *testing.T) { s := runner.NewInProcessSuite(t, sharedNet, runner.WithSetupScript(timelockedFixture), + runner.WithSetupEnv(timelockedSetupEnv), runner.WithIsolatedKeyring()) s.RunFile("../wasm_module/timelocked_token_delegation_test.yaml") s.RunFile("../wasm_module/timelocked_token_admin_test.yaml") @@ -210,15 +215,25 @@ func TestInProcessWasmModuleCore(t *testing.T) { func TestInProcessWasmModuleEmergencyWithdraw(t *testing.T) { s := runner.NewInProcessSuite(t, sharedNet, runner.WithSetupScript(timelockedFixture), + runner.WithSetupEnv(timelockedSetupEnv), runner.WithIsolatedKeyring()) s.RunFile("../wasm_module/timelocked_token_emergency_withdraw_test.yaml") } -// TestInProcessFlatKVEvmModule is skipped in-process: flatkv_evm_test.yaml asserts a -// docker fixture — a pre-deployed EVM contract with recorded balances/heights read -// from integration_test/contracts/flatkv_evm_*.txt — not built by the shared network. +// TestInProcessFlatKVEvmModule deploys the flatkv EVM fixture (a storage contract, +// an EVM transfer, and admin associate-address — all via cast + seid) then runs the +// historical --block balance/storage/code queries against it. It runs on the shared +// network: SeiDB SC+SS (see appoptions.go) retain the recorded heights, and the +// fixture adds no keyring names, so no isolation is needed. BULK_STORAGE_KEYS=0 +// skips the ~80-block bulk deploy the YAML never asserts. Needs cast (Foundry) on PATH. func TestInProcessFlatKVEvmModule(t *testing.T) { - t.Skip("seidb flatkv_evm asserts a docker fixture (pre-deployed EVM contract + recorded balances/heights)") + runner.RunFile(t, "../seidb/flatkv_evm_test.yaml", + runner.WithInProcessNetwork(sharedNet), + runner.WithSetupScript("integration_test/contracts/deploy_flatkv_evm_fixture.sh"), + runner.WithSetupEnv(map[string]string{ + "FLATKV_EVM_FIXTURE_KEYRING_BACKEND": "test", + "FLATKV_EVM_BULK_STORAGE_KEYS": "0", + })) } // TestInProcessAuthzModule runs the three authz suites, each with an isolated diff --git a/integration_test/seidb/flatkv_evm_test.yaml b/integration_test/seidb/flatkv_evm_test.yaml index 4cf716837f..64c9ef8626 100644 --- a/integration_test/seidb/flatkv_evm_test.yaml +++ b/integration_test/seidb/flatkv_evm_test.yaml @@ -6,7 +6,7 @@ env: BALANCE_HEIGHT - cmd: tail -1 integration_test/contracts/flatkv_evm_balance_expected.txt env: EXPECTED_BALANCE - - cmd: cast to-hex $(cast balance $RECIPIENT_ADDR --block $BALANCE_HEIGHT --rpc-url http://localhost:8545) + - cmd: cast to-hex $(cast balance $RECIPIENT_ADDR --block $BALANCE_HEIGHT --rpc-url ${EVM_RPC_URL:-http://localhost:8545}) env: ACTUAL_BALANCE verifiers: - type: eval @@ -22,7 +22,7 @@ env: CONTRACT_HEIGHT - cmd: tail -1 integration_test/contracts/flatkv_evm_storage_expected.txt env: EXPECTED_STORAGE - - cmd: cast storage $CONTRACT_ADDR $STORAGE_SLOT --block $CONTRACT_HEIGHT --rpc-url http://localhost:8545 + - cmd: cast storage $CONTRACT_ADDR $STORAGE_SLOT --block $CONTRACT_HEIGHT --rpc-url ${EVM_RPC_URL:-http://localhost:8545} env: ACTUAL_STORAGE verifiers: - type: eval @@ -36,7 +36,7 @@ env: CONTRACT_HEIGHT - cmd: tail -1 integration_test/contracts/flatkv_evm_code_expected.txt env: EXPECTED_CODE - - cmd: cast code $CONTRACT_ADDR --block $CONTRACT_HEIGHT --rpc-url http://localhost:8545 + - cmd: cast code $CONTRACT_ADDR --block $CONTRACT_HEIGHT --rpc-url ${EVM_RPC_URL:-http://localhost:8545} env: ACTUAL_CODE verifiers: - type: eval @@ -54,9 +54,9 @@ env: EXPECTED_MISSING_BALANCE - cmd: tail -1 integration_test/contracts/flatkv_evm_missing_storage_expected.txt env: EXPECTED_MISSING_STORAGE - - cmd: cast to-hex $(cast balance $MISSING_ADDR --block $CONTRACT_HEIGHT --rpc-url http://localhost:8545) + - cmd: cast to-hex $(cast balance $MISSING_ADDR --block $CONTRACT_HEIGHT --rpc-url ${EVM_RPC_URL:-http://localhost:8545}) env: ACTUAL_MISSING_BALANCE - - cmd: cast storage $MISSING_ADDR $STORAGE_SLOT --block $CONTRACT_HEIGHT --rpc-url http://localhost:8545 + - cmd: cast storage $MISSING_ADDR $STORAGE_SLOT --block $CONTRACT_HEIGHT --rpc-url ${EVM_RPC_URL:-http://localhost:8545} env: ACTUAL_MISSING_STORAGE verifiers: - type: eval @@ -74,9 +74,9 @@ env: EXPECTED_STORAGE - cmd: tail -1 integration_test/contracts/flatkv_evm_code_expected.txt env: EXPECTED_CODE - - cmd: cast storage $CONTRACT_ADDR $STORAGE_SLOT --block latest --rpc-url http://localhost:8545 + - cmd: cast storage $CONTRACT_ADDR $STORAGE_SLOT --block latest --rpc-url ${EVM_RPC_URL:-http://localhost:8545} env: ACTUAL_LATEST_STORAGE - - cmd: cast code $CONTRACT_ADDR --block latest --rpc-url http://localhost:8545 + - cmd: cast code $CONTRACT_ADDR --block latest --rpc-url ${EVM_RPC_URL:-http://localhost:8545} env: ACTUAL_LATEST_CODE verifiers: - type: eval From dd35a0ca99ad4bb90f85726f54f373e2d772d59b Mon Sep 17 00:00:00 2001 From: bdchatham Date: Tue, 7 Jul 2026 09:34:03 -0700 Subject: [PATCH 5/7] test(inprocess): migrate SeiDB state_store; multi-script setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the SeiDB state_store historical-query suite (wasm list-code 3/13/23/33 + tokenfactory denom counts 0/10/20/30 at recorded heights, plus code-ids). The EVM module's InitGenesis stores 3 CW-pointer codes as ids 1-3 on every fresh chain, so the fixtures' 30 codes are the deterministic ids 4-33 the suite asserts. Runs on the shared net; SC+SS retain the historical heights (proven by FlatKVEvm), ~127s. - Generalize WithSetupScript → WithSetupScripts (variadic): runSetup runs an ordered list of fixtures. SetupScript → SetupScripts; the setupRunner hook reads opts. - Env-default deploy_wasm_contracts.sh + create_tokenfactory_denoms.sh (SEIDBIN, FIXTURE_SIGNER, keyaddress derived from the signer) and gate the inter-set sleeps (FIXTURE_SETTLE_SECONDS) — the seq-poll gates commits, so in-process sets it to 0. Docker byte-identical when the vars are unset. - Run both fixtures as a fresh genesis key seidb_creator, not admin: the counts need a creator with no prior codes/denoms and the tokenfactory suite pollutes admin. - deploy_wasm_contracts.sh guards a pristine baseline (max code id == 3), failing loudly if another suite stored wasm first (shuffle/-count/reorder) or the pointer set changed — converting an ordering landmine into an explicit contract. Full in-process suite green (12 suites), clean worktree. Co-Authored-By: Claude Opus 4.8 --- .../contracts/create_tokenfactory_denoms.sh | 12 +++--- .../contracts/deploy_wasm_contracts.sh | 25 ++++++++++--- integration_test/runner/runner.go | 34 ++++++++--------- integration_test/runner/runner_inprocess.go | 34 +++++++++-------- .../runner/runner_inprocess_test.go | 37 ++++++++++++++----- 5 files changed, 90 insertions(+), 52 deletions(-) diff --git a/integration_test/contracts/create_tokenfactory_denoms.sh b/integration_test/contracts/create_tokenfactory_denoms.sh index b90b20d7d2..e4a708da7a 100755 --- a/integration_test/contracts/create_tokenfactory_denoms.sh +++ b/integration_test/contracts/create_tokenfactory_denoms.sh @@ -1,8 +1,10 @@ #!/bin/bash -seidbin=$(which ~/go/bin/seid | tr -d '"') -keyname=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"') -keyaddress=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].address" | tr -d '"') +# SEIDBIN / FIXTURE_SIGNER let a non-docker caller repoint the binary + signer +# without changing docker's behavior (both unset → the original computed values). +seidbin=${SEIDBIN:-$(which ~/go/bin/seid | tr -d '"')} +keyname=${FIXTURE_SIGNER:-$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"')} +keyaddress=$(printf "12345678\n" | $seidbin keys show "$keyname" -a | tr -d '"') chainid=$($seidbin status | jq ".NodeInfo.network" | tr -d '"') seihome=$(git rev-parse --show-toplevel | tr -d '"') @@ -53,7 +55,7 @@ done first_set_block_height=$($seidbin status | jq -r '.SyncInfo.latest_block_height') echo "$first_set_block_height" > $seihome/integration_test/contracts/tfk_first_set_block_height.txt -sleep 5 +sleep "${FIXTURE_SETTLE_SECONDS:-5}" # create second set of tokenfactory denoms for i in {11..20} @@ -66,7 +68,7 @@ done second_set_block_height=$($seidbin status | jq -r '.SyncInfo.latest_block_height') echo "$second_set_block_height" > $seihome/integration_test/contracts/tfk_second_set_block_height.txt -sleep 5 +sleep "${FIXTURE_SETTLE_SECONDS:-5}" # create third set of tokenfactory denoms for i in {21..30} diff --git a/integration_test/contracts/deploy_wasm_contracts.sh b/integration_test/contracts/deploy_wasm_contracts.sh index 0bf7271797..425c7cad3a 100755 --- a/integration_test/contracts/deploy_wasm_contracts.sh +++ b/integration_test/contracts/deploy_wasm_contracts.sh @@ -1,14 +1,29 @@ #!/bin/bash -seidbin=$(which ~/go/bin/seid | tr -d '"') -keyname=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"') -keyaddress=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].address" | tr -d '"') +# SEIDBIN / FIXTURE_SIGNER let a non-docker caller repoint the binary + signer +# without changing docker's behavior (both unset → the original computed values). +seidbin=${SEIDBIN:-$(which ~/go/bin/seid | tr -d '"')} +keyname=${FIXTURE_SIGNER:-$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"')} +keyaddress=$(printf "12345678\n" | $seidbin keys show "$keyname" -a | tr -d '"') chainid=$($seidbin status | jq ".NodeInfo.network" | tr -d '"') seihome=$(git rev-parse --show-toplevel | tr -d '"') source "$(dirname "$0")/../utils/_tx_helpers.sh" cd $seihome || exit + +# The suite's absolute code-ids (4/20) and counts (3/13/23/33) assume these are the +# only wasm codes after the 3 CW-pointer codes the EVM module's InitGenesis stores +# (ids 1-3). Fail loudly here if that isn't so — another wasm-storing suite ran first +# (e.g. `go test -shuffle`/-count, or a reorder), or the pointer set changed — rather +# than with a cryptic count mismatch later. Docker runs this first on a fresh cluster, +# so max == 3 there too. +max_code_id=$(_get_max_wasm_code_id) +if [ "$max_code_id" != "3" ]; then + echo "seidb wasm fixture needs a pristine chain: max code id must be 3 (the baseline), got $max_code_id — another suite stored wasm first" >&2 + exit 1 +fi + echo "Deploying first set of contracts..." beginning_block_height=$($seidbin status | jq -r '.SyncInfo.latest_block_height') @@ -27,7 +42,7 @@ done first_set_block_height=$($seidbin status | jq -r '.SyncInfo.latest_block_height') echo "$first_set_block_height" > $seihome/integration_test/contracts/wasm_first_set_block_height.txt -sleep 5 +sleep "${FIXTURE_SETTLE_SECONDS:-5}" # store second set of contracts for i in {11..20} @@ -41,7 +56,7 @@ done second_set_block_height=$($seidbin status | jq -r '.SyncInfo.latest_block_height') echo "$second_set_block_height" > $seihome/integration_test/contracts/wasm_second_set_block_height.txt -sleep 5 +sleep "${FIXTURE_SETTLE_SECONDS:-5}" # store third set of contracts for i in {21..30} diff --git a/integration_test/runner/runner.go b/integration_test/runner/runner.go index 3e889f9c91..6592e418bd 100644 --- a/integration_test/runner/runner.go +++ b/integration_test/runner/runner.go @@ -79,13 +79,13 @@ type keyringIsolator interface { } // setupRunner is an optional execer capability: a backend that can run a suite's -// fixture bring-up script (deploy contracts, seed keys the cases assume) once +// fixture bring-up scripts (deploy contracts, seed keys the cases assume) once // implements it, and RunFile invokes it after keyring isolation and before any -// case when Options.SetupScript is set. The in-process arm runs the script +// case when Options.SetupScripts is set. The in-process arm runs the scripts // through its seid shim; the docker arm (fixtures brought up by the CI harness) // does not implement it. type setupRunner interface { - runSetup(t *testing.T, scriptPath string, opts Options) error + runSetup(t *testing.T, opts Options) error } // Options controls how RunFile executes commands. @@ -112,14 +112,14 @@ type Options struct { // (per-container keyrings) ignores it. IsolateKeyring bool - // SetupScript, when set, is a repo-root-relative fixture bring-up script run - // once before the cases (deploy the contracts + seed the keys the suites - // assume). Backend-specific: the in-process arm runs it through its shim - // (executed at the repo root, so the path is repo-root-relative); the docker - // arm ignores it (its fixtures are brought up by the CI harness). - SetupScript string + // SetupScripts are repo-root-relative fixture bring-up scripts run in order, + // once, before the cases (deploy the contracts + seed the keys the suites + // assume). Backend-specific: the in-process arm runs them through its shim + // (executed at the repo root, so paths are repo-root-relative); the docker arm + // ignores them (its fixtures are brought up by the CI harness). + SetupScripts []string - // SetupEnv is fixture-specific environment for the SetupScript run (e.g. a + // SetupEnv is fixture-specific environment for the setup-script run (e.g. a // keyring backend, a signer name, an RPC target). The in-process arm layers it // under its own node-targeting env (SEID_HOME/SEID_NODE/EVM endpoints). SetupEnv map[string]string @@ -156,13 +156,13 @@ func WithIsolatedKeyring() Option { return func(o *Options) { o.IsolateKeyring = true } } -// WithSetupScript runs a fixture bring-up script once before the cases (see -// Options.SetupScript). The path is repo-root-relative. -func WithSetupScript(path string) Option { - return func(o *Options) { o.SetupScript = path } +// WithSetupScripts runs one or more fixture bring-up scripts, in order, once before +// the cases (see Options.SetupScripts). Paths are repo-root-relative. +func WithSetupScripts(paths ...string) Option { + return func(o *Options) { o.SetupScripts = paths } } -// WithSetupEnv sets fixture-specific environment for the SetupScript run (see +// WithSetupEnv sets fixture-specific environment for the setup-script run (see // Options.SetupEnv). func WithSetupEnv(env map[string]string) Option { return func(o *Options) { o.SetupEnv = env } @@ -208,9 +208,9 @@ func runSuiteSetup(t *testing.T, o Options) { require.NoError(t, iso.isolateKeyring(t), "isolate keyring") } } - if o.SetupScript != "" { + if len(o.SetupScripts) > 0 { if sr, ok := o.exec.(setupRunner); ok { - require.NoError(t, sr.runSetup(t, o.SetupScript, o), "run setup script") + require.NoError(t, sr.runSetup(t, o), "run setup scripts") } } } diff --git a/integration_test/runner/runner_inprocess.go b/integration_test/runner/runner_inprocess.go index 01cf6852a4..d8814b325a 100644 --- a/integration_test/runner/runner_inprocess.go +++ b/integration_test/runner/runner_inprocess.go @@ -46,7 +46,7 @@ type InProcessSuite struct { // NewInProcessSuite binds net, runs the one-time setup once (see runSuiteSetup for // hook ordering), and returns a suite whose RunFile reuses it. Pass the setup -// options (WithSetupScript, WithIsolatedKeyring); the network is bound here, so +// options (WithSetupScripts, WithIsolatedKeyring); the network is bound here, so // WithInProcessNetwork is not needed. Setup and every RunFile run on t, so the // keyring overlay and seid binary outlive all subtests. func NewInProcessSuite(t *testing.T, net *inprocess.Network, opts ...Option) *InProcessSuite { @@ -223,29 +223,31 @@ func (e *inProcessExecer) isolateKeyring(t *testing.T) error { return nil } -// runSetup is the setupRunner hook: it runs a suite's fixture script once through -// the same shimmed environment the cases use (bare `seid` lands on the target -// node; the node's EVM endpoint in EVM_RPC_URL; CWD at the repo root), before any -// case, with the caller's fixture-specific opts.SetupEnv layered on top. Unlike -// run, a non-zero script exit is fatal: a failed fixture must fail the suite, not -// silently leave the cases to assert against missing state. -func (e *inProcessExecer) runSetup(t *testing.T, scriptPath string, opts Options) error { +// runSetup is the setupRunner hook: it runs the suite's fixture scripts in order, +// once, through the same shimmed environment the cases use (bare `seid` lands on +// the target node; the node's EVM endpoint in EVM_RPC_URL; CWD at the repo root), +// before any case, with the caller's fixture-specific opts.SetupEnv layered on top. +// Unlike run, a non-zero script exit is fatal: a failed fixture must fail the +// suite, not silently leave the cases to assert against missing state. +func (e *inProcessExecer) runSetup(t *testing.T, opts Options) error { t.Helper() if err := e.ensureBin(t); err != nil { return fmt.Errorf("prepare seid: %w", err) } // Fixtures write outputs into the repo tree; register cleanup first so a clean - // worktree is restored even if the script below fails partway. + // worktree is restored even if a script below fails partway. if err := e.cleanFixtureOutputs(t); err != nil { return err } - // node "" → node 0 (admin's home), the suites' default signing home. - c, err := e.command(t, "bash "+scriptPath, "", opts.SetupEnv, opts) - if err != nil { - return err - } - if out, err := c.CombinedOutput(); err != nil { - return fmt.Errorf("setup script %s: %w\n%s", scriptPath, err, out) + for _, script := range opts.SetupScripts { + // node "" → node 0 (admin's home), the suites' default signing home. + c, err := e.command(t, "bash "+script, "", opts.SetupEnv, opts) + if err != nil { + return err + } + if out, err := c.CombinedOutput(); err != nil { + return fmt.Errorf("setup script %s: %w\n%s", script, err, out) + } } return nil } diff --git a/integration_test/runner/runner_inprocess_test.go b/integration_test/runner/runner_inprocess_test.go index 9264886093..6ec721ca9e 100644 --- a/integration_test/runner/runner_inprocess_test.go +++ b/integration_test/runner/runner_inprocess_test.go @@ -81,6 +81,10 @@ func runSuites(m *testing.M) int { TimeoutCommit: time.Second, ExtraKeys: []inprocess.ExtraKey{ {Name: "admin", Node: 0, Coins: adminFunding()}, + // seidb_creator is a pristine wasm/tokenfactory creator for the SeiDB + // suite: its historical counts assume a creator with no prior codes/denoms, + // but `admin` is polluted (the tokenfactory suite creates a denom as admin). + {Name: "seidb_creator", Node: 0, Coins: adminFunding()}, }, }) if err != nil { @@ -165,12 +169,27 @@ func TestInProcessOracleModule(t *testing.T) { runner.RunFile(t, "../oracle_module/verify_penalty_counts.yaml", runner.WithInProcessNetwork(sharedNet)) } -// TestInProcessSeiDBModule is skipped in-process: state_store_test.yaml asserts a -// docker fixture — 300 wasm contracts stored across block heights tracked in -// integration_test/contracts/wasm_*_block_height.txt — which the shared harness -// network doesn't build, so every historical list-code query returns empty. +// TestInProcessSeiDBModule runs the state_store historical-query suite (wasm +// list-code + tokenfactory denom counts at recorded heights). The EVM module's +// InitGenesis stores 3 CW-pointer codes (erc20/721/1155) as ids 1-3 on every fresh +// chain — same as docker — so the fixtures' 30 codes are the deterministic ids 4-33 +// the suite asserts; deploy_wasm_contracts.sh guards that baseline (fails loudly if +// a pointer is ever added/removed, or another suite stored wasm first). Both run as +// seidb_creator, not admin (a pristine creator; see runSuites). +// FIXTURE_SETTLE_SECONDS=0 drops the docker KV-indexer sleeps — the seq-poll gates +// commits, and --height queries don't care about settle time. func TestInProcessSeiDBModule(t *testing.T) { - t.Skip("seidb state_store asserts a docker fixture (300 wasm contracts at tracked heights)") + runner.RunFile(t, "../seidb/state_store_test.yaml", + runner.WithInProcessNetwork(sharedNet), + runner.WithSetupScripts( + "integration_test/contracts/deploy_wasm_contracts.sh", + "integration_test/contracts/create_tokenfactory_denoms.sh", + ), + runner.WithSetupEnv(map[string]string{ + "SEIDBIN": "seid", + "FIXTURE_SIGNER": "seidb_creator", + "FIXTURE_SETTLE_SECONDS": "0", + })) } // TestInProcessBankMultiSig builds a 2-of-3 multisig (fresh wallet1/2/3 keys) and @@ -187,7 +206,7 @@ func TestInProcessBankSimulation(t *testing.T) { // timelockedFixture is the gringotts bring-up docker runs before each wasm group // (deploy goblin + gringotts, seed admin1-4/op/etc, record the addresses). -// WithSetupScript runs it in-process through the seid shim; WithIsolatedKeyring +// WithSetupScripts runs it in-process through the seid shim; WithIsolatedKeyring // gives each group its own overlay so the fixture's repeated `keys add admin1` // doesn't hit the override prompt on the second deploy. const timelockedFixture = "integration_test/contracts/deploy_timelocked_token_contract.sh" @@ -201,7 +220,7 @@ var timelockedSetupEnv = map[string]string{"SEIDBIN": "seid", "FIXTURE_SIGNER": // suite's single deploy + keyring; order matters (withdraw depends on prior state). func TestInProcessWasmModuleCore(t *testing.T) { s := runner.NewInProcessSuite(t, sharedNet, - runner.WithSetupScript(timelockedFixture), + runner.WithSetupScripts(timelockedFixture), runner.WithSetupEnv(timelockedSetupEnv), runner.WithIsolatedKeyring()) s.RunFile("../wasm_module/timelocked_token_delegation_test.yaml") @@ -214,7 +233,7 @@ func TestInProcessWasmModuleCore(t *testing.T) { // runs before TestWasmModuleEmergencyWithdraw. func TestInProcessWasmModuleEmergencyWithdraw(t *testing.T) { s := runner.NewInProcessSuite(t, sharedNet, - runner.WithSetupScript(timelockedFixture), + runner.WithSetupScripts(timelockedFixture), runner.WithSetupEnv(timelockedSetupEnv), runner.WithIsolatedKeyring()) s.RunFile("../wasm_module/timelocked_token_emergency_withdraw_test.yaml") @@ -229,7 +248,7 @@ func TestInProcessWasmModuleEmergencyWithdraw(t *testing.T) { func TestInProcessFlatKVEvmModule(t *testing.T) { runner.RunFile(t, "../seidb/flatkv_evm_test.yaml", runner.WithInProcessNetwork(sharedNet), - runner.WithSetupScript("integration_test/contracts/deploy_flatkv_evm_fixture.sh"), + runner.WithSetupScripts("integration_test/contracts/deploy_flatkv_evm_fixture.sh"), runner.WithSetupEnv(map[string]string{ "FLATKV_EVM_FIXTURE_KEYRING_BACKEND": "test", "FLATKV_EVM_BULK_STORAGE_KEYS": "0", From 54c3999baeaa496518922fc16af24bbda93660eb Mon Sep 17 00:00:00 2001 From: bdchatham Date: Tue, 7 Jul 2026 12:19:30 -0700 Subject: [PATCH 6/7] test(inprocess): migrate EVM Module (Compat) hardhat suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run the hardhat EVM-compatibility suite in-process against the shared network's node 0, no docker (~186s vs the 5.5-min docker job). The first non-YAML-runner driver: InProcessEVMEnv gives a hardhat/npm process the seid shim on PATH + the node's EVM RPC, the seam the rest of the EVM-hardhat class reuses. - InProcessEVMEnv(t, net, node): shim on PATH (lib.js funds signers via bare seid, whose isDocker() falls through to it), SEID_HOME/SEID_NODE, SEI_EVM_RPC/EVM_RPC_URL, SEI_IN_PROCESS. TestInProcessEVMModuleCompat shells `npx hardhat test`. - lib.js evmSend appends --evm-rpc (the CLI has no env default; docker byte-identical when SEI_EVM_RPC unset); hardhat.config seilocal.url + 3 chainId-spec axios URLs env-repoint to the node; the one gov-param-change spec is env-gated (same docker-gov coupling as the skipped gov suite). - harness: set consensus block max_gas/max_gas_wanted to the docker localnode's 35M/70M pair (defaults are unlimited → EVM reports a 100M gaslimit the suite rejects, and 50M wanted is too tight for the 35M cap). Set on the started genDoc since collectGentxs re-export resets ConsensusParams. Full in-process suite green (12 YAML suites + EVM Compat), no docker regression (all JS edits env-default to docker behavior). Co-Authored-By: Claude Opus 4.8 --- contracts/hardhat.config.js | 4 ++- contracts/test/EVMCompatabilityTest.js | 10 +++++--- contracts/test/lib.js | 7 +++++- inprocess/harness.go | 11 ++++++++ .../runner/runner_evm_inprocess_test.go | 25 +++++++++++++++++++ integration_test/runner/runner_inprocess.go | 24 ++++++++++++++++++ 6 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 integration_test/runner/runner_evm_inprocess_test.go diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index d1657da3c7..745e0755d3 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -27,7 +27,9 @@ module.exports = { accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e"], // Replace with your private key }, seilocal: { - url: "http://127.0.0.1:8545", + // SEI_EVM_RPC lets a non-docker runner point at the node's dynamic EVM + // endpoint; unset → the docker localnode's in-container :8545. + url: process.env.SEI_EVM_RPC || "http://127.0.0.1:8545", address: ["0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8","0x817E1414b633948e50101Df0b722DeA5f8C29109"], accounts: ["0x57acb95d82739866a5c29e40b0aa2590742ae50425b7dd5b5d279a986370189e", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d","0x888432482e2cbcf4e2b248a388f2a6d9fe7b59a11e9136fd615942d7421e89bf"], }, diff --git a/contracts/test/EVMCompatabilityTest.js b/contracts/test/EVMCompatabilityTest.js index f191d3ce22..24d160647f 100644 --- a/contracts/test/EVMCompatabilityTest.js +++ b/contracts/test/EVMCompatabilityTest.js @@ -1327,7 +1327,7 @@ describe("EVM Validations ", function() { gasLimit: 21000, maxFeePerGas: 400000000000}) - const nodeUrl = 'http://localhost:8545'; + const nodeUrl = process.env.SEI_EVM_RPC || 'http://localhost:8545'; const response = await axios.post(nodeUrl, { method: 'eth_sendRawTransaction', params: [signedTx], @@ -1351,7 +1351,7 @@ describe("EVM Validations ", function() { gasPrice: 1000000000000, gasLimit: 21000}) - const nodeUrl = 'http://localhost:8545'; + const nodeUrl = process.env.SEI_EVM_RPC || 'http://localhost:8545'; const response = await axios.post(nodeUrl, { method: 'eth_sendRawTransaction', params: [signedTx], @@ -1374,7 +1374,7 @@ describe("EVM Validations ", function() { gasPrice: 1000000000000, gasLimit: 21000}) - const nodeUrl = 'http://localhost:8545'; + const nodeUrl = process.env.SEI_EVM_RPC || 'http://localhost:8545'; const response = await axios.post(nodeUrl, { method: 'eth_sendRawTransaction', params: [signedTx], @@ -1568,7 +1568,9 @@ describe("SSTORE Gas Discrepancy Test", function() { } }); - it("should reproduce mismatch by changing param between execution and trace", async function() { + // Skipped in-process: drives an EVM-param gov proposal that assumes the docker + // localnode's gov topology (same coupling as the skipped YAML gov suite). + (process.env.SEI_IN_PROCESS ? it.skip : it)("should reproduce mismatch by changing param between execution and trace", async function() { this.timeout(120000); // 2 minutes for governance proposals const { proposeParamChange, passProposal } = require("./lib.js"); diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 640889a790..f39c612b8f 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -5,6 +5,11 @@ const crypto = require("crypto"); const adminKeyName = "admin" +// SEI_EVM_RPC (set by the in-process runner) points `seid tx evm` eth-submitting +// commands at the node's dynamic EVM endpoint. Unset under docker → empty, so the +// CLI's in-container :8545 default stands and the docker commands are byte-identical. +const evmRpcFlag = process.env.SEI_EVM_RPC ? ` --evm-rpc ${process.env.SEI_EVM_RPC}` : "" + const ABI = { ERC20: [ "function name() view returns (string)", @@ -142,7 +147,7 @@ async function evmSend(addr, fromKey, amount="10000000000000000000000000") { // seid tx evm send prints "Transaction hash: 0x..." on its own format // (not the standard cosmos JSON response), so we extract from text and // wait via the JSON-RPC receipt — semantically equivalent to -b block. - const output = await execute(`seid tx evm send ${addr} ${amount} --from ${fromKey} -b sync -y`); + const output = await execute(`seid tx evm send ${addr} ${amount} --from ${fromKey} -b sync -y${evmRpcFlag}`); const evmTxHash = output.replace(/.*0x/, "0x").trim() await waitForReceipt(evmTxHash) return evmTxHash diff --git a/inprocess/harness.go b/inprocess/harness.go index 4e85253980..52191225ac 100644 --- a/inprocess/harness.go +++ b/inprocess/harness.go @@ -391,6 +391,17 @@ func (net *Network) startNode(ctx context.Context, n *node, enc encoding) error {PubKey: tmPub, Address: tmPub.Address(), Name: n.moniker, Power: 100}, } } + // Match the docker localnode's block-gas pair — 35M max_gas / 70M max_gas_wanted + // (its 2x ratio avoids false-positive gas rejections). The defaults are unlimited + // max_gas (which the EVM reports as a 100M block gaslimit) and 50M max_gas_wanted + // (too tight for the 35M cap — a batch whose declared gas exceeds 50M but used gas + // stays under 35M would fail ProcessProposal's checkTotalBlockGas). Set here, on the + // genDoc the node starts with, because collectGentxs's re-export resets ConsensusParams. + if genDoc.ConsensusParams == nil { + genDoc.ConsensusParams = tmtypes.DefaultConsensusParams() + } + genDoc.ConsensusParams.Block.MaxGas = 35_000_000 + genDoc.ConsensusParams.Block.MaxGasWanted = 70_000_000 tmNode, err := tmnode.New( ctx, n.tmCfg, func() {}, theApp, genDoc, diff --git a/integration_test/runner/runner_evm_inprocess_test.go b/integration_test/runner/runner_evm_inprocess_test.go new file mode 100644 index 0000000000..992470c966 --- /dev/null +++ b/integration_test/runner/runner_evm_inprocess_test.go @@ -0,0 +1,25 @@ +//go:build inprocess + +// In-process arm for the EVM suites driven by hardhat/npm rather than the YAML +// runner. See runner.InProcessEVMEnv. +package runner_test + +import ( + "os" + "os/exec" + "testing" + + "github.com/sei-protocol/sei-chain/integration_test/runner" +) + +// TestInProcessEVMModuleCompat runs the hardhat EVM-compatibility suite against node 0 +// via InProcessEVMEnv. The suite's bare `seid` funding works because lib.js's +// isDocker() falls through to the shimmed seid on PATH. +func TestInProcessEVMModuleCompat(t *testing.T) { + cmd := exec.Command("npx", "hardhat", "test", "--network", "seilocal", "test/EVMCompatabilityTest.js") + cmd.Dir = "../../contracts" + cmd.Env = append(os.Environ(), runner.InProcessEVMEnv(t, sharedNet, 0)...) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("hardhat EVM compat suite failed: %v\n%s", err, out) + } +} diff --git a/integration_test/runner/runner_inprocess.go b/integration_test/runner/runner_inprocess.go index d8814b325a..a25d99430b 100644 --- a/integration_test/runner/runner_inprocess.go +++ b/integration_test/runner/runner_inprocess.go @@ -33,6 +33,30 @@ func WithInProcessNetwork(net *inprocess.Network) Option { return withExecer(newInProcessExecer(net)) } +// InProcessEVMEnv builds the environment a host EVM tool (hardhat, cast) needs to +// target net's node without docker: the seid shim on PATH (so a suite's bare `seid` +// funding/association calls hit that node), SEID_HOME/SEID_NODE for the shim, and +// SEI_EVM_RPC/EVM_RPC_URL for the tool's own JSON-RPC. Returned as KEY=VALUE entries +// to append to os.Environ(). This is the seam for the EVM suites whose driver is +// hardhat/npm rather than the YAML runner. +func InProcessEVMEnv(t *testing.T, net *inprocess.Network, node int) []string { + t.Helper() + e := newInProcessExecer(net) + if err := e.ensureBin(t); err != nil { // go's build cache makes a repeat build cheap + t.Fatalf("build seid shim: %v", err) + } + h := net.Node(node) + return []string{ + "PATH=" + e.binDir + string(os.PathListSeparator) + os.Getenv("PATH"), + "SEID_HOME=" + h.Home(), + "SEID_NODE=" + h.RPCNodeAddr(), + "SEI_EVM_RPC=" + h.EVMRPC(), + "EVM_RPC_URL=" + h.EVMRPC(), + // Signals suites to skip specs needing the docker localnode's gov topology. + "SEI_IN_PROCESS=1", + } +} + // InProcessSuite runs several YAML files against one shared network with a single // one-time setup (build → optional keyring overlay → optional fixture script), // then reuses it for every RunFile. Use it when a group of files shares fixture From 718000e516f216635d974ddd5c275e1673e89d9d Mon Sep 17 00:00:00 2001 From: bdchatham Date: Tue, 7 Jul 2026 13:31:23 -0700 Subject: [PATCH 7/7] =?UTF-8?q?test(inprocess):=20Phase=20B=20=E2=80=94=20?= =?UTF-8?q?mint/startup/gov=20via=20N=3D4=20localnode-parity=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a public inprocess.Options genesis surface + a dedicated N=4 package that runs the Mint/startup/gov suites in-process, no docker (the last major query/tx class). Options surface (opt-in; nil/empty → current behavior): - GenesisUseiSupply *sdk.Int → creditReserve credits a keyless reserve account to hit an exact total supply (bank derives supply from balances), decoupled from N. - MintTokenReleaseSchedule []MintRelease (day-offsets) → applyMintSchedule writes the schedule + mint_denom=usei and pins the epoch clock to one mintRef, so the first release fires on the schedule's start date (kills the two-clock date race). - GovParams (+ DockerLocalnodeGovParams) → applyGovParams overrides voting/tally so the gov suites resolve (default 2-day voting never does). All three baseState edits survive collectGentxs (it re-folds only genutil); the max_gas pair lives in harness.go because that's a genDoc-header field the re-export resets. integration_test/runner_localnode: N=4 TestMain, mint/startup/gov. Gov scoped to param-change + migration + expedited (gov_param_and_expedited_test.yaml); the rejected-burn + staking_proposal cases (absolute-supply / duplicate-expedited) stay on docker. startup's valset query counts occurrences (grep -o) to survive the in-process JSON output format. Oracle/slashing left at defaults — oracle jailing is off by default (feeder retired), so the 4-validator set never churns. All three green (mint ~90s, startup ~20s, gov ~125s); shared N=3 net unaffected (mutations no-op when Options unset). Co-Authored-By: Claude Opus 4.8 --- inprocess/genesis.go | 97 ++++++++++++++ inprocess/harness.go | 65 +++++++++- .../gov_param_and_expedited_test.yaml | 120 ++++++++++++++++++ .../runner_localnode/runner_localnode_test.go | 113 +++++++++++++++++ integration_test/startup/startup_test.yaml | 2 +- 5 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 integration_test/gov_module/gov_param_and_expedited_test.yaml create mode 100644 integration_test/runner_localnode/runner_localnode_test.go diff --git a/inprocess/genesis.go b/inprocess/genesis.go index b8e5188ece..2512118407 100644 --- a/inprocess/genesis.go +++ b/inprocess/genesis.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/sei-protocol/sei-chain/sei-cosmos/client" "github.com/sei-protocol/sei-chain/sei-cosmos/client/tx" @@ -19,9 +20,12 @@ import ( banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/genutil" genutiltypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/genutil/types" + govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" tmtime "github.com/sei-protocol/sei-chain/sei-tendermint/libs/time" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" + epochtypes "github.com/sei-protocol/sei-chain/x/epoch/types" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" ) // genesisBuilder accumulates per-validator accounts, balances, and gentxs across @@ -43,6 +47,12 @@ type genesisBuilder struct { accounts []authtypes.GenesisAccount balances []banktypes.Balance + + // Genesis-parity knobs (from Options); zero values skip the mutation. + useiSupplyTarget *sdk.Int + mintSchedule []MintRelease + govParams *GovParams + mintRef time.Time // one instant driving both the mint schedule + epoch clock } // operatorKeyName is the keyring name each node's validator operator key is stored @@ -138,6 +148,12 @@ func (b *genesisBuilder) fundAccount( // writeBaseGenesis writes a base genesis file (accounts + balances, empty // validator set) to every validator's genesis path. Mirrors initGenFiles. func (b *genesisBuilder) writeBaseGenesis(baseState map[string]json.RawMessage, genFiles []string) error { + // Credit the reserve (if any) before the auth+bank fold so both the account and + // its balance land in the marshaled state below. + if err := b.creditReserve(); err != nil { + return err + } + var authGenState authtypes.GenesisState b.codec.MustUnmarshalJSON(baseState[authtypes.ModuleName], &authGenState) packed, err := authtypes.PackAccounts(b.accounts) @@ -152,6 +168,9 @@ func (b *genesisBuilder) writeBaseGenesis(baseState map[string]json.RawMessage, bankGenState.Balances = append(bankGenState.Balances, b.balances...) baseState[banktypes.ModuleName] = b.codec.MustMarshalJSON(&bankGenState) + b.applyMintSchedule(baseState) + b.applyGovParams(baseState) + appStateJSON, err := json.MarshalIndent(baseState, "", " ") if err != nil { return err @@ -169,6 +188,84 @@ func (b *genesisBuilder) writeBaseGenesis(baseState map[string]json.RawMessage, return nil } +// creditReserve appends a fixed keyless reserve account + balance so total genesis +// usei == useiSupplyTarget. Bank InitGenesis derives supply from balances (writeBaseGenesis +// leaves bank.Supply empty), so the supply invariant holds. No-op when the target is nil. +func (b *genesisBuilder) creditReserve() error { + if b.useiSupplyTarget == nil { + return nil + } + existing := sdk.ZeroInt() + for _, bal := range b.balances { + existing = existing.Add(bal.Coins.AmountOf(b.bondDenom)) + } + shortfall := b.useiSupplyTarget.Sub(existing) + if shortfall.IsNegative() { + return fmt.Errorf("inprocess: GenesisUseiSupply %s is below already-funded %s %s", b.useiSupplyTarget, b.bondDenom, existing) + } + // A stable, collision-free address deriver; never signed from, so no keyring entry + // and a plain BaseAccount (not a registered ModuleAccount). + addr := authtypes.NewModuleAddress("inprocess_genesis_reserve") + b.accounts = append(b.accounts, authtypes.NewBaseAccount(addr, nil, 0, 0)) + if shortfall.IsPositive() { + b.balances = append(b.balances, banktypes.Balance{ + Address: addr.String(), + Coins: sdk.NewCoins(sdk.NewCoin(b.bondDenom, shortfall)), + }) + } + return nil +} + +// applyMintSchedule rewrites the mint genesis with the schedule (dates resolved from +// mintRef) + mint_denom, and pins the epoch clock to mintRef so the first epoch's +// CurrentEpochStartTime shares the schedule's start UTC date — the two must be derived +// from one instant, or the mint suite's START_DATE==LAST_MINT_DATE assertion races +// across midnight. No-op when the schedule is empty. +func (b *genesisBuilder) applyMintSchedule(baseState map[string]json.RawMessage) { + if len(b.mintSchedule) == 0 { + return + } + day := b.mintRef.Truncate(24 * time.Hour) + sched := make([]minttypes.ScheduledTokenRelease, 0, len(b.mintSchedule)) + for _, r := range b.mintSchedule { + sched = append(sched, minttypes.ScheduledTokenRelease{ + StartDate: day.AddDate(0, 0, r.StartDaysFromGenesis).Format(minttypes.TokenReleaseDateFormat), + EndDate: day.AddDate(0, 0, r.EndDaysFromGenesis).Format(minttypes.TokenReleaseDateFormat), + TokenReleaseAmount: r.Amount.Uint64(), + }) + } + var mintGen minttypes.GenesisState + b.codec.MustUnmarshalJSON(baseState[minttypes.ModuleName], &mintGen) + mintGen.Params.MintDenom = b.bondDenom + mintGen.Params.TokenReleaseSchedule = sched + baseState[minttypes.ModuleName] = b.codec.MustMarshalJSON(&mintGen) + + var epochGen epochtypes.GenesisState + b.codec.MustUnmarshalJSON(baseState[epochtypes.ModuleName], &epochGen) + epochGen.Epoch.GenesisTime = b.mintRef + epochGen.Epoch.CurrentEpochStartTime = b.mintRef + baseState[epochtypes.ModuleName] = b.codec.MustMarshalJSON(&epochGen) +} + +// applyGovParams overrides the gov voting/deposit/tally params. No-op when nil; the +// default 2-day voting period never resolves the gov suites' short-sleep votes. +func (b *genesisBuilder) applyGovParams(baseState map[string]json.RawMessage) { + if b.govParams == nil { + return + } + p := b.govParams + var govGen govtypes.GenesisState + b.codec.MustUnmarshalJSON(baseState[govtypes.ModuleName], &govGen) + govGen.VotingParams.VotingPeriod = p.VotingPeriod + govGen.VotingParams.ExpeditedVotingPeriod = p.ExpeditedVotingPeriod + govGen.DepositParams.MaxDepositPeriod = p.MaxDepositPeriod + govGen.TallyParams.Quorum = p.Quorum + govGen.TallyParams.Threshold = p.Threshold + govGen.TallyParams.ExpeditedQuorum = p.ExpeditedQuorum + govGen.TallyParams.ExpeditedThreshold = p.ExpeditedThreshold + baseState[govtypes.ModuleName] = b.codec.MustMarshalJSON(&govGen) +} + // collectGentxs folds every validator's gentx into each node's genesis app state // under one canonical genesis time (consensus timestamp validation diverges if // the nodes disagree on GenesisTime). Mirrors collectGenFiles. diff --git a/inprocess/harness.go b/inprocess/harness.go index 52191225ac..ca3de07947 100644 --- a/inprocess/harness.go +++ b/inprocess/harness.go @@ -84,6 +84,59 @@ type Options struct { // validator operator the harness provisions (see operatorKeyName), and // provisionExtraKeys rejects it. ExtraKeys []ExtraKey + + // GenesisUseiSupply, when non-nil, pins total genesis usei supply to this exact + // amount: after validators + ExtraKeys are funded, a fixed keyless reserve account + // is credited (target - sum of existing usei), decoupling supply-parity from + // validator count. nil = supply is just the sum of funded balances (current behavior). + GenesisUseiSupply *sdk.Int + + // MintTokenReleaseSchedule, when non-empty, writes the mint token_release_schedule + // (dates as day-offsets from genesis, mint_denom pinned to usei) and pins the epoch + // clock so the first release fires on the schedule's start date. Empty = mint stays + // at its module default. + MintTokenReleaseSchedule []MintRelease + + // GovParams, when non-nil, overrides the gov module's voting/deposit/tally params. + // Required for the gov suites: the default 2-day voting period never resolves their + // short-sleep vote cases. + GovParams *GovParams +} + +// MintRelease is one token_release_schedule entry, expressed as day-offsets from the +// network's genesis date. Offsets (not absolute date strings) let the harness derive +// the schedule and the epoch clock from one reference instant, so the first release +// fires on the schedule's start date deterministically. +type MintRelease struct { + StartDaysFromGenesis int // 0 = genesis day + EndDaysFromGenesis int // must be > StartDaysFromGenesis + Amount sdk.Int // token_release_amount +} + +// GovParams overrides the gov module genesis params. nil leaves the module default. +type GovParams struct { + VotingPeriod time.Duration + ExpeditedVotingPeriod time.Duration + MaxDepositPeriod time.Duration + Quorum sdk.Dec + Threshold sdk.Dec + ExpeditedQuorum sdk.Dec + ExpeditedThreshold sdk.Dec +} + +// DockerLocalnodeGovParams returns the gov params the docker localnode sets +// (step2_genesis.sh): 0.5/0.5 normal quorum/threshold, 0.9/0.9 expedited, and +// 30s/15s/100s voting/expedited/deposit periods — what the gov suites resolve against. +func DockerLocalnodeGovParams() *GovParams { + return &GovParams{ + VotingPeriod: 30 * time.Second, + ExpeditedVotingPeriod: 15 * time.Second, + MaxDepositPeriod: 100 * time.Second, + Quorum: sdk.MustNewDecFromStr("0.5"), + Threshold: sdk.MustNewDecFromStr("0.5"), + ExpeditedQuorum: sdk.MustNewDecFromStr("0.9"), + ExpeditedThreshold: sdk.MustNewDecFromStr("0.9"), + } } // ExtraKey is a non-validator genesis account the harness creates and funds — a @@ -194,10 +247,14 @@ func Start(ctx context.Context, opts Options) (_ *Network, retErr error) { enc := app.MakeEncodingConfig() gb := &genesisBuilder{ - codec: enc.Marshaler, - txConfig: enc.TxConfig, - chainID: opts.ChainID, - bondDenom: sdk.DefaultBondDenom, + codec: enc.Marshaler, + txConfig: enc.TxConfig, + chainID: opts.ChainID, + bondDenom: sdk.DefaultBondDenom, + useiSupplyTarget: opts.GenesisUseiSupply, + mintSchedule: opts.MintTokenReleaseSchedule, + govParams: opts.GovParams, + mintRef: time.Now().UTC(), // one reference instant for the mint schedule + epoch clock } if err := net.provisionNodes(enc, gb); err != nil { diff --git a/integration_test/gov_module/gov_param_and_expedited_test.yaml b/integration_test/gov_module/gov_param_and_expedited_test.yaml new file mode 100644 index 0000000000..fed8f9b306 --- /dev/null +++ b/integration_test/gov_module/gov_param_and_expedited_test.yaml @@ -0,0 +1,120 @@ +- name: Test making a new param change proposal should pass and take effect + inputs: + # Get the current tally params + - cmd: seid q gov params --output json | jq -r .tally_params.quorum + env: OLD_PARAM + # Make a new proposal + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_gov_proposal admin "Gov Param Change" gov submit-proposal param-change ./integration_test/gov_module/proposal/param_change_proposal.json --fees 2000usei + env: PROPOSAL_ID + # Get proposal status + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Make a deposit + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait admin gov deposit $PROPOSAL_ID 10000000usei --fees 2000usei + # sei-node-0 vote yes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-0 + # sei-node-1 vote yes + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-1 + # since quorum is 0.5, we only need 2/4 votes and expect proposal to pass after 35 seconds + - cmd: sleep 35 + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Get the tally params again after proposal is passed + - cmd: seid q gov params --output json | jq -r .tally_params.quorum + env: NEW_TALLY_PARAM + - cmd: seid q params subspace baseapp ABCIParams --output json |jq -r .value |jq .recheck_tx + env: NEW_ABCI_PARAM + verifiers: + # Check if the new params matches the expected value after proposal + - type: eval + expr: NEW_TALLY_PARAM == 0.450000000000000000 + - type: eval + expr: NEW_ABCI_PARAM == "true" + +# Scope: this case verifies the governance plumbing for the migration rate — +# that a ParameterChangeProposal lands NumKeysToMigratePerBlock in the migration +# subspace and the new value is readable afterwards. The actual auto +# memiavl_only -> migrate_evm kick-off and the full cross-validator key-drain / +# digest agreement are covered separately by +# integration_test/contracts/verify_flatkv_evm_migrate.sh; asserting the +# one-time transition here is not idempotent across re-runs of a long-lived +# cluster, so this case intentionally stays scoped to the param plumbing. +- name: Test migration param change proposal should update NumKeysToMigratePerBlock + inputs: + # Get the current migration batch size param (defaults to 0 = paused) + - cmd: seid q params subspace migration NumKeysToMigratePerBlock --output json | jq -r .value | tr -d "\"" + env: OLD_PARAM + # Make a new proposal to raise the migration rate + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_gov_proposal admin "Update Migration Batch Size" gov submit-proposal param-change ./integration_test/gov_module/proposal/migration_param_change_proposal.json --fees 2000usei + env: PROPOSAL_ID + # Get proposal status + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Make a deposit + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait admin gov deposit $PROPOSAL_ID 10000000usei --fees 2000usei + # sei-node-0 vote yes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-0 + # sei-node-1 vote yes + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-1 + # since quorum is 0.5, we only need 2/4 votes and expect proposal to pass after 35 seconds + - cmd: sleep 35 + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Get the migration batch size param again after proposal is passed + - cmd: seid q params subspace migration NumKeysToMigratePerBlock --output json | jq -r .value | tr -d "\"" + env: NEW_PARAM + verifiers: + # The proposal must have passed + - type: eval + expr: PROPOSAL_STATUS == "PROPOSAL_STATUS_PASSED" + # The migration batch size param must reflect the new value. The value is a + # string (piped through tr -d "\""), so quote the literal to match the + # sibling verifiers; the eval engine coerces both sides to big.Int anyway. + - type: eval + expr: NEW_PARAM == "12345" + +- name: Test expedited proposal should respect expedited_voting_period + inputs: + # Get the current tally params + - cmd: seid q gov params --output json | jq -r .tally_params.expedited_quorum + env: OLD_PARAM + # Make a new expedited proposal + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_gov_proposal admin "Gov Param Change Expedited" gov submit-proposal param-change ./integration_test/gov_module/proposal/expedited_proposal.json --fees 2000usei + env: PROPOSAL_ID + # Get proposal status + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Make a deposit + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait admin gov deposit $PROPOSAL_ID 10000000usei --fees 2000usei + # sei-node-0 vote yes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-0 + # sei-node-1 vote yes + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-1 + # sei-node-2 vote yes + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-2 + # sei-node-3 vote yes + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei + node: sei-node-3 + # since expedited quorum is 0.9, we only need 4/4 votes and expect expedited proposal to pass after 20 seconds + - cmd: sleep 20 + - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + env: PROPOSAL_STATUS + # Get the tally params again after proposal is passed + - cmd: seid q gov params --output json | jq -r .tally_params.expedited_quorum + env: NEW_PARAM + verifiers: + # Check if the new params is the expected value after proposal + - type: eval + expr: NEW_PARAM == 0.750000000000000000 diff --git a/integration_test/runner_localnode/runner_localnode_test.go b/integration_test/runner_localnode/runner_localnode_test.go new file mode 100644 index 0000000000..4325f69b14 --- /dev/null +++ b/integration_test/runner_localnode/runner_localnode_test.go @@ -0,0 +1,113 @@ +//go:build inprocess + +// Package runner_localnode_test runs the docker-localnode-parity suites (mint, +// startup, gov param-change + expedited) against a dedicated N=4 in-process network +// whose genesis matches step2_genesis.sh — 5e21 usei supply, the ~3-day mint window, +// 4 validators, and the docker gov params. It is a separate package/process from the +// N=3 shared net (runner_inprocess_test.go) because the harness allows one network +// per process and this genesis differs. +// +// Parity is scoped to supply/mint/gov: oracle + slashing params are left at module +// defaults, which already disable oracle jailing (feeder retired → min-valid-per-window +// 0) and put downtime jailing ~28h out — so the 4-validator set never churns during a +// run. A suite asserting oracle/slashing behavior would need those params replicated. +// +// go test -tags inprocess -v -timeout 900s ./integration_test/runner_localnode/ +package runner_localnode_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + + "github.com/sei-protocol/sei-chain/inprocess" + "github.com/sei-protocol/sei-chain/integration_test/runner" +) + +const chainID = "sei" + +// mustInt parses an integer literal, panicking on a bad constant (a programming error). +func mustInt(literal string) sdk.Int { + amt, ok := sdk.NewIntFromString(literal) + if !ok { + panic("bad int literal: " + literal) + } + return amt +} + +// usei is a usei-coin amount from an integer literal. +func usei(literal string) sdk.Coins { return sdk.NewCoins(sdk.NewCoin("usei", mustInt(literal))) } + +// sharedNet is the one N=4 network the localnode-parity suites run against; TestMain +// owns its lifecycle. Suites run sequentially (no t.Parallel) — one chain, shared state. +var sharedNet *inprocess.Network + +func TestMain(m *testing.M) { os.Exit(runSuites(m)) } + +func runSuites(m *testing.M) int { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) + defer cancel() + + supply := mustInt("5000000000000000000000") // 5e21: 4×1e21 validators + 1e21 admin + net, err := inprocess.Start(ctx, inprocess.Options{ + Validators: 4, // startup asserts valset==4; gov needs 4 equal-power validators + ChainID: chainID, + TimeoutCommit: time.Second, + GenesisUseiSupply: &supply, // → TOTAL_SUPPLY 5000000000333333333333 after one mint release + MintTokenReleaseSchedule: []inprocess.MintRelease{ + {StartDaysFromGenesis: 0, EndDaysFromGenesis: 3, Amount: sdk.NewInt(999999999999)}, + }, + GovParams: inprocess.DockerLocalnodeGovParams(), + ExtraKeys: []inprocess.ExtraKey{ + {Name: "admin", Node: 0, Coins: usei("1000000000000000000000")}, // gov deposits + fees + }, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "inprocess.Start: %v\n", err) + return 1 + } + defer net.Close() + if err := net.WaitReady(ctx); err != nil { + fmt.Fprintf(os.Stderr, "WaitReady: %v\n", err) + return 1 + } + sharedNet = net + return m.Run() +} + +// TestLocalnodeMint asserts the exact post-first-release supply. Exactly one release +// fires per UTC day (the minter dedups same-day), and mint_test's own sleep crosses the +// first epoch (~65s), so the read sees exactly one — stable regardless of suite order. +// skipIfNearUTCMidnight avoids a run straddling midnight, which would fire a second. +func TestLocalnodeMint(t *testing.T) { + skipIfNearUTCMidnight(t) + runner.RunFile(t, "../mint_module/mint_test.yaml", runner.WithInProcessNetwork(sharedNet)) +} + +// TestLocalnodeStartup asserts the 4-validator topology; time-independent. +func TestLocalnodeStartup(t *testing.T) { + runner.RunFile(t, "../startup/startup_test.yaml", runner.WithInProcessNetwork(sharedNet)) +} + +// TestLocalnodeGov runs the param-change + expedited cases (the scoped YAML drops the +// rejected-burn case, which asserts an absolute supply the long-running net has moved +// past). Votes span sei-node-0..3 — 2/4 clears quorum 0.5, 4/4 clears expedited 0.9. +func TestLocalnodeGov(t *testing.T) { + runner.RunFile(t, "../gov_module/gov_param_and_expedited_test.yaml", runner.WithInProcessNetwork(sharedNet)) +} + +// skipIfNearUTCMidnight skips within slack of UTC midnight: the mint schedule is +// date-keyed, so a network lifetime crossing midnight can observe a second daily +// release, breaking the exact TOTAL_SUPPLY/REMAINING assertions. A once-a-day skip, +// not a flake. +func skipIfNearUTCMidnight(t *testing.T) { + const slack = 5 * time.Minute + now := time.Now().UTC() + if nextMidnight := now.Truncate(24 * time.Hour).Add(24 * time.Hour); nextMidnight.Sub(now) < slack { + t.Skipf("within %s of UTC midnight; the date-keyed mint schedule would flake", slack) + } +} diff --git a/integration_test/startup/startup_test.yaml b/integration_test/startup/startup_test.yaml index bb9ff5fc61..3d94d640ae 100644 --- a/integration_test/startup/startup_test.yaml +++ b/integration_test/startup/startup_test.yaml @@ -1,7 +1,7 @@ - name: Test number of validators should be equal to 4 inputs: # Query num of validators - - cmd: seid q tendermint-validator-set |grep address |wc -l + - cmd: seid q tendermint-validator-set |grep -o address |wc -l env: RESULT verifiers: - type: eval