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/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 ccb7b8fee2..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 { @@ -391,6 +448,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, @@ -451,6 +519,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/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_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/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/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/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 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/runner.go b/integration_test/runner/runner.go index d5b2e5cfb2..6592e418bd 100644 --- a/integration_test/runner/runner.go +++ b/integration_test/runner/runner.go @@ -70,6 +70,24 @@ 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 +} + +// setupRunner is an optional execer capability: a backend that can run a suite's +// 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.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, 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. @@ -84,6 +102,27 @@ 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 + + // 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 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 } // Option is a functional option for Options. @@ -111,6 +150,24 @@ 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 } +} + +// 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 setup-script 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 @@ -130,12 +187,38 @@ 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") } + if o.IsolateKeyring { + if iso, ok := o.exec.(keyringIsolator); ok { + require.NoError(t, iso.isolateKeyring(t), "isolate keyring") + } + } + if len(o.SetupScripts) > 0 { + if sr, ok := o.exec.(setupRunner); ok { + require.NoError(t, sr.runSetup(t, o), "run setup scripts") + } + } +} + +// 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_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 2e05b62ce0..a25d99430b 100644 --- a/integration_test/runner/runner_inprocess.go +++ b/integration_test/runner/runner_inprocess.go @@ -33,6 +33,63 @@ 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 +// 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 (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 { + 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"` @@ -45,6 +102,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 { @@ -61,45 +124,65 @@ 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 + 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. "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(), ) - - 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 @@ -132,6 +215,93 @@ 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 +} + +// 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 a script below fails partway. + if err := e.cleanFixtureOutputs(t); err != nil { + return err + } + 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 +} + +// 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 + } + t.Cleanup(func() { + // -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 +} + // 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..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 @@ -185,28 +204,69 @@ 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). +// 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" + +// 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.WithSetupScripts(timelockedFixture), + runner.WithSetupEnv(timelockedSetupEnv), + 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") } -// 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. +// 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.WithSetupScripts(timelockedFixture), + runner.WithSetupEnv(timelockedSetupEnv), + runner.WithIsolatedKeyring()) + s.RunFile("../wasm_module/timelocked_token_emergency_withdraw_test.yaml") +} + +// 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.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", + })) } -// 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()) + } } 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/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 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