diff --git a/app/ante/cosmos_checktx.go b/app/ante/cosmos_checktx.go index e9311860a8..eeefe62921 100644 --- a/app/ante/cosmos_checktx.go +++ b/app/ante/cosmos_checktx.go @@ -241,11 +241,6 @@ func CosmosStatelessChecks(tx sdk.Tx, height int64, consensusParams *tmproto.Con func SetGasMeter(ctx sdk.Context, gasLimit uint64, paramsKeeper paramskeeper.Keeper) sdk.Context { cosmosGasParams := paramsKeeper.GetCosmosGasParams(ctx) - - if ctx.BlockHeight() == 0 { - return ctx.WithGasMeter(storetypes.NewInfiniteMultiplierGasMeter(cosmosGasParams.CosmosGasMultiplierNumerator, cosmosGasParams.CosmosGasMultiplierDenominator)) - } - return ctx.WithGasMeter(storetypes.NewMultiplierGasMeter(gasLimit, cosmosGasParams.CosmosGasMultiplierNumerator, cosmosGasParams.CosmosGasMultiplierDenominator)) } @@ -450,15 +445,10 @@ func CheckSignatures(ctx sdk.Context, txConfig client.TxConfig, tx sdk.Tx, signe } // retrieve signer data - genesis := ctx.BlockHeight() == 0 chainID := ctx.ChainID() - var accNum uint64 - if !genesis { - accNum = signerAcc.GetAccountNumber() - } signerData := authsigning.SignerData{ ChainID: chainID, - AccountNumber: accNum, + AccountNumber: signerAcc.GetAccountNumber(), Sequence: signerAcc.GetSequence(), } @@ -468,9 +458,9 @@ func CheckSignatures(ctx sdk.Context, txConfig client.TxConfig, tx sdk.Tx, signe if authante.OnlyLegacyAminoSigners(sig.Data) { // If all signers are using SIGN_MODE_LEGACY_AMINO, we rely on VerifySignature to check account sequence number, // and therefore communicate sequence number as a potential cause of error. - errMsg = fmt.Sprintf("signature verification failed; please verify account number (%d), sequence (%d) and chain-id (%s)", accNum, signerAcc.GetSequence(), chainID) + errMsg = fmt.Sprintf("signature verification failed; please verify account number (%d), sequence (%d) and chain-id (%s)", signerAcc.GetAccountNumber(), signerAcc.GetSequence(), chainID) } else { - errMsg = fmt.Sprintf("signature verification failed; please verify account number (%d) and chain-id (%s)", accNum, chainID) + errMsg = fmt.Sprintf("signature verification failed; please verify account number (%d) and chain-id (%s)", signerAcc.GetAccountNumber(), chainID) } return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, errMsg) diff --git a/app/antedecorators/gas.go b/app/antedecorators/gas.go index 8f45e47cc9..98656a187a 100644 --- a/app/antedecorators/gas.go +++ b/app/antedecorators/gas.go @@ -10,8 +10,8 @@ func GetGasMeterSetter(pk paramskeeper.Keeper) func(bool, sdk.Context, uint64, s return func(simulate bool, ctx sdk.Context, gasLimit uint64, tx sdk.Tx) sdk.Context { cosmosGasParams := pk.GetCosmosGasParams(ctx) - // In simulation, still use multiplier but with infinite gas limit - if simulate || ctx.BlockHeight() == 0 { + // In simulation and genesis delivery, still use multiplier but with infinite gas limit. + if simulate || ctx.IsGenesis() { return ctx.WithGasMeter(types.NewInfiniteMultiplierGasMeter(cosmosGasParams.CosmosGasMultiplierNumerator, cosmosGasParams.CosmosGasMultiplierDenominator)) } diff --git a/app/app.go b/app/app.go index caea548446..a56e574160 100644 --- a/app/app.go +++ b/app/app.go @@ -476,11 +476,6 @@ type App struct { forkInitializer func(sdk.Context) - httpServerStartSignal chan struct{} - wsServerStartSignal chan struct{} - httpServerStartSignalSent bool - wsServerStartSignalSent bool - // evmHTTPServer/evmWSServer hold the EVM JSON-RPC HTTP and WebSocket listeners // constructed in RegisterLocalServices so an embedding orchestrator (the // in-process harness) can Stop() them at teardown. Nil when the respective @@ -539,21 +534,19 @@ func New( memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey, banktypes.DeferredCacheStoreKey, oracletypes.MemStoreKey) app := &App{ - BaseApp: bApp, - cdc: cdc, - appCodec: appCodec, - interfaceRegistry: interfaceRegistry, - keys: keys, - tkeys: tkeys, - memKeys: memKeys, - txDecoder: encodingConfig.TxConfig.TxDecoder(), - versionInfo: version.NewInfo(), - metricCounter: &map[string]float32{}, - encodingConfig: encodingConfig, - legacyEncodingConfig: MakeLegacyEncodingConfig(), - stateStore: stateStore, - httpServerStartSignal: make(chan struct{}, 1), - wsServerStartSignal: make(chan struct{}, 1), + BaseApp: bApp, + cdc: cdc, + appCodec: appCodec, + interfaceRegistry: interfaceRegistry, + keys: keys, + tkeys: tkeys, + memKeys: memKeys, + txDecoder: encodingConfig.TxConfig.TxDecoder(), + versionInfo: version.NewInfo(), + metricCounter: &map[string]float32{}, + encodingConfig: encodingConfig, + legacyEncodingConfig: MakeLegacyEncodingConfig(), + stateStore: stateStore, } for _, option := range appOptions { @@ -1946,17 +1939,6 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req *BlockProcessReq } }() - defer func() { - if !app.httpServerStartSignalSent { - app.httpServerStartSignalSent = true - app.httpServerStartSignal <- struct{}{} - } - if !app.wsServerStartSignalSent { - app.wsServerStartSignalSent = true - app.wsServerStartSignal <- struct{}{} - } - }() - ctx = ctx.WithIsOCCEnabled(app.OccEnabled()) blockSpanCtx, blockSpan := app.GetBaseApp().TracingInfo.Start("Block") @@ -2640,19 +2622,7 @@ func (app *App) LegacyAmino() *codec.LegacyAmino { } func (app *App) GetValidators() []abci.ValidatorUpdate { - // AUTOBAHN: After InitChain but before the first Commit, the committed - // store is empty — staking params don't exist, so reading from committed - // store panics in MaxValidators. Use DeliverContext when available at - // height 0, since it has the uncommitted staking state from InitChain. - // CometBFT consensus never hits this because its handshaker commits - // after InitChain before any block processing begins. - if app.LastBlockHeight() == 0 { - if dctx := app.DeliverContext(); dctx != nil { - return app.StakingKeeper.GetBondedValidators(*dctx) - } - } - ctx := app.NewUncachedContext(false, tmproto.Header{Height: max(app.LastBlockHeight(), 1)}) - return app.StakingKeeper.GetBondedValidators(ctx) + return app.StakingKeeper.GetBondedValidators(app.GetCheckCtx()) } // AppCodec returns an app codec. @@ -2811,7 +2781,7 @@ func (app *App) RegisterLocalServices(node client.LocalClient, txConfig client.T } app.evmHTTPServer = evmHTTPServer go func() { - <-app.httpServerStartSignal + <-app.Initialized() if err := evmHTTPServer.Start(); err != nil { panic(err) } @@ -2826,7 +2796,7 @@ func (app *App) RegisterLocalServices(node client.LocalClient, txConfig client.T } app.evmWSServer = evmWSServer go func() { - <-app.wsServerStartSignal + <-app.Initialized() if err := evmWSServer.Start(); err != nil { panic(err) } diff --git a/app/eth_replay.go b/app/eth_replay.go index 9a29330749..732ff58fe2 100644 --- a/app/eth_replay.go +++ b/app/eth_replay.go @@ -35,7 +35,7 @@ func Replay(a *App) { if err != nil { panic(err) } - _, err = a.InitChain(context.Background(), &abci.RequestInitChain{ + _, err = a.InitChain(&abci.RequestInitChain{ Time: time.Now(), ChainId: gendoc.ChainID, AppStateBytes: gendoc.AppState, @@ -119,7 +119,7 @@ func BlockTest(a *App, bt *ethtests.BlockTest) { if err != nil { panic(err) } - _, err = a.InitChain(context.Background(), &abci.RequestInitChain{ + _, err = a.InitChain(&abci.RequestInitChain{ Time: time.Now(), ChainId: gendoc.ChainID, AppStateBytes: gendoc.AppState, diff --git a/app/test_helpers.go b/app/test_helpers.go index bdaa60f237..e33512e322 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -397,12 +397,11 @@ func SetupWithAppOptsAndDefaultHome(isCheckTx bool, appOpts TestAppOpts, enableE // TODO: remove once init chain works with SC defer func() { _ = recover() }() - _, err = res.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - ChainId: "sei-test", - AppStateBytes: stateBytes, - }, + _, err = res.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + ChainId: "sei-test", + AppStateBytes: stateBytes, + }, ) if err != nil { panic(err) @@ -466,12 +465,11 @@ func SetupWithDB(tb testing.TB, db dbm.DB, isCheckTx bool, enableEVMCustomPrecom panic(err) } - _, err = res.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - ChainId: "sei-test", - AppStateBytes: stateBytes, - }, + _, err = res.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + ChainId: "sei-test", + AppStateBytes: stateBytes, + }, ) if err != nil { panic(err) @@ -514,12 +512,11 @@ func SetupWithScReceiptFromOpts(t *testing.T, isCheckTx bool, enableEVMCustomPre defer func() { _ = recover() }() - _, err = res.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - ChainId: "sei-test", - AppStateBytes: stateBytes, - }, + _, err = res.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + ChainId: "sei-test", + AppStateBytes: stateBytes, + }, ) if err != nil { panic(err) @@ -570,11 +567,10 @@ func SetupWithSc(t *testing.T, isCheckTx bool, enableEVMCustomPrecompiles bool, // TODO: remove once init chain works with SC defer func() { _ = recover() }() - _, err = res.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - AppStateBytes: stateBytes, - }, + _, err = res.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + AppStateBytes: stateBytes, + }, ) if err != nil { panic(err) @@ -614,12 +610,11 @@ func SetupTestingAppWithLevelDb(t *testing.T, isCheckTx bool, enableEVMCustomPre panic(err) } - _, err = app.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - ChainId: "sei-test", - AppStateBytes: stateBytes, - }, + _, err = app.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + ChainId: "sei-test", + AppStateBytes: stateBytes, + }, ) if err != nil { panic(err) @@ -741,12 +736,11 @@ func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs require.NoError(t, err) // init chain will set the validator set and initialize the genesis accounts - _, _ = app.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - ChainId: "sei-test", - AppStateBytes: stateBytes, - }, + _, _ = app.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + ChainId: "sei-test", + AppStateBytes: stateBytes, + }, ) // commit genesis changes @@ -783,12 +777,11 @@ func SetupWithGenesisAccounts(t *testing.T, genAccs []authtypes.GenesisAccount, panic(err) } - _, _ = app.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - ChainId: "sei-test", - AppStateBytes: stateBytes, - }, + _, _ = app.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + ChainId: "sei-test", + AppStateBytes: stateBytes, + }, ) _, _ = app.Commit(context.Background()) diff --git a/assets/SeiLogo.png b/assets/SeiLogo.png deleted file mode 100644 index 601706f264..0000000000 Binary files a/assets/SeiLogo.png and /dev/null differ diff --git a/contracts/test/EVMCompatabilityTest.js b/contracts/test/EVMCompatabilityTest.js index f191d3ce22..66f3aeb6af 100644 --- a/contracts/test/EVMCompatabilityTest.js +++ b/contracts/test/EVMCompatabilityTest.js @@ -4,7 +4,7 @@ const {uniq} = require("lodash"); const hre = require('hardhat'); const { ethers, upgrades } = hre; const { getImplementationAddress } = require('@openzeppelin/upgrades-core'); -const { deployEvmContract, setupSigners, fundAddress, waitForBaseFeeToEq, waitForBaseFeeToBeGt} = require("./lib") +const { deployEvmContract, setupSigners, fundAddress, mineTransferBlock, waitForReceipt, waitForCondition } = require("./lib") const axios = require("axios"); const { default: BigNumber } = require("bignumber.js"); @@ -198,14 +198,14 @@ describe("EVM Test", function () { }; const result = await ethers.provider.call(tx); - // wait for block to change - while(true){ - const bn = await ethers.provider.getBlockNumber(); - if(bn !== currentBlockNumber){ - break - } - await sleep(100) - } + // Force a real block instead of waiting for idle block production. + const advanceTx = await owner.sendTransaction({ + to: owner.address, + value: 1n, + gasPrice: ethers.parseUnits('100', 'gwei'), + }); + await advanceTx.wait(); + const result2 = await ethers.provider.call(tx); expect(result).to.equal(result2) }); @@ -673,7 +673,7 @@ describe("EVM Test", function () { it("Check base fee on the right block", async function () { await delay() - // check if base fee is 1gwei, otherwise wait + // Drive real blocks until base fee decays back to 1 gwei. let iterations = 0 while (true) { const block = await ethers.provider.getBlock("latest"); @@ -685,10 +685,11 @@ describe("EVM Test", function () { if(iterations > 10) { throw new Error("base fee hasn't dropped to 1gwei in 10 iterations") } - await sleep(1000); + await mineTransferBlock(owner); } - // use at least 1000000 gas to increase base fee - const txResponse = await evmTester.useGas(1000000, { gasPrice: ethers.parseUnits('100', 'gwei') }); + // Fill the parent block heavily enough that the next block's base fee + // must rise above the 1 gwei floor. + const txResponse = await evmTester.useGas(9500000, { gasPrice: ethers.parseUnits('100', 'gwei') }); const receipt = await txResponse.wait(); const blockHeight = receipt.blockNumber; @@ -697,15 +698,17 @@ describe("EVM Test", function () { const block = await ethers.provider.getBlock(blockHeight); const baseFee = Number(block.baseFeePerGas); expect(baseFee).to.equal(oneGwei); - // wait for the next block - while (true) { - const bn = await ethers.provider.getBlockNumber(); - if(bn !== blockHeight){ - break - } - await sleep(500) - } - const nextBlock = await ethers.provider.getBlock(blockHeight + 1); + // Progress-only block: the assertion is specifically about child + // block H+1 of the heavy tx's block H, so force progress and then + // read that exact child block once it exists. + const nextReceipt = await mineTransferBlock(owner); + expect(nextReceipt.blockNumber).to.be.greaterThan(blockHeight); + const nextBlockHeight = blockHeight + 1; + await waitForCondition( + async () => (await ethers.provider.getBlock(nextBlockHeight)) !== null, + `block ${nextBlockHeight} to exist`, + ); + const nextBlock = await ethers.provider.getBlock(nextBlockHeight); const nextBaseFee = Number(nextBlock.baseFeePerGas); expect(nextBaseFee).to.be.greaterThan(oneGwei); }); @@ -983,7 +986,7 @@ describe("EVM Test", function () { // stops working once the chain grows past that (notably under // Autobahn's faster block production). const tx = await evmTester.emitDummyEvent("test", 0, { gasPrice: ethers.parseUnits('100', 'gwei') }); - const receipt = await tx.wait(); + const receipt = await waitForReceipt(tx.hash); const filter = { fromBlock: receipt.blockNumber, toBlock: receipt.blockNumber, @@ -1011,8 +1014,8 @@ describe("EVM Test", function () { let blockEnd; let numTxs = 5; before(async function() { - await sleep(5000); // wait for a block to pass so we get a fresh block number - blockStart = await ethers.provider.getBlockNumber(); + const startReceipt = await mineTransferBlock(owner); + blockStart = startReceipt.blockNumber; // Emit an event by making a transaction for (let i = 0; i < numTxs; i++) { diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 640889a790..8d539559ce 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -78,6 +78,17 @@ async function delay() { await sleep(1000) } +async function mineTransferBlock(sender) { + // Progress-only self-transfer: under allow_empty_blocks=false, tests must + // actively create a block when they need "the next block" to exist. + const tx = await sender.sendTransaction({ + to: sender.address, + value: 1n, + gasPrice: ethers.parseUnits('100', 'gwei'), + }) + return await waitForReceipt(tx.hash) +} + // Default 2 because the very next block after submit can be empty // Like provider.getTransactionReceipt, but treats the Autobahn-specific // "requested height N is not yet available; safe latest is N-1" @@ -93,26 +104,13 @@ async function tryGetReceipt(provider, txHash) { } } -// (tx still in mempool, lands one block later). -async function waitForBlocks(blocks=2, timeoutMs=15000) { - const start = await ethers.provider.getBlockNumber() - const deadline = Date.now() + timeoutMs - while (Date.now() < deadline) { - const cur = await ethers.provider.getBlockNumber() - if (cur >= start + blocks) return - await sleep(50) - } - throw new Error(`block didn't advance by ${blocks} in ${timeoutMs}ms (start=${start})`) -} - // Poll an arbitrary side-effect check until it returns truthy. Used by // helpers that need to wait for a -b sync tx to take effect: instead of // polling `seid q tx ` (which doesn't work under Autobahn — the -// cosmos tx indexer isn't wired) or `waitForBlocks` (temporal, not -// causal), each caller passes a closure that queries the actual state -// it cares about (e.g. account balance, denom existence). Works under -// both Autobahn and legacy because the check goes through whatever query -// path the caller already relies on. +// cosmos tx indexer isn't wired), each caller passes a closure that +// queries the actual state it cares about (e.g. account balance, denom +// existence). Works under both Autobahn and legacy because the check +// goes through whatever query path the caller already relies on. async function waitForCondition(check, description, timeoutMs=30000, intervalMs=200) { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { @@ -142,7 +140,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} --evm-rpc=http://localhost:8545 -b sync -y`); const evmTxHash = output.replace(/.*0x/, "0x").trim() await waitForReceipt(evmTxHash) return evmTxHash @@ -262,16 +260,60 @@ async function getKeySeiAddress(name) { return (await execute(`seid keys show ${name} -a`)).trim() } +async function waitForProposalStatus( + proposalId, + targetStatus, + { kickKeyName = adminKeyName, pollIntervalMs = 1000 } = {}, +) { + const readProposal = async () => JSON.parse(await execute(`seid q gov proposal ${proposalId} -o json`)) + const ensureNotFailed = (status) => { + if ( + (status === "PROPOSAL_STATUS_REJECTED" || status === "PROPOSAL_STATUS_FAILED") && + status !== targetStatus + ) { + throw new Error(`Proposal ${proposalId} was rejected/failed with status: ${status}`) + } + } + + const kickAddr = await getKeySeiAddress(kickKeyName) + + while (true) { + const proposal = await readProposal() + if (proposal.status === targetStatus) { + return proposal + } + ensureNotFailed(proposal.status) + + const votingEndMs = Date.parse(proposal.voting_end_time ?? "") + if (!Number.isFinite(votingEndMs)) { + throw new Error(`Proposal ${proposalId} is missing a valid voting_end_time`) + } + + if (Date.now() >= votingEndMs + 1000) { + // Progress-only bank send: once the currently observed voting end + // time has passed, force one committed block so tallying can + // advance. Expedited proposals can convert to regular and extend + // voting_end_time, so re-read proposal state after every kick. + await bankSend(kickAddr, kickKeyName, "1", "usei") + } + + await sleep(pollIntervalMs) + } +} + // Best-effort helper for idempotent bootstrap paths that may run after an // address is already associated. async function associateKey(keyName) { try { + const seiAddress = await getKeySeiAddress(keyName) // seid tx evm associate-address has a custom (non-cosmos-JSON) output // format. The try/catch already tolerates failure here, and subsequent - // associate calls will succeed once the chain catches up, so a temporal - // wait is acceptable. + // associate calls will succeed once the chain catches up. await execute(`seid tx evm associate-address --from ${keyName} -b sync`) - await waitForBlocks() + await waitForCondition( + async () => (await getEvmAddressAssociation(seiAddress)).associated === true, + `${seiAddress} to have an associated EVM address`, + ) } catch (e) { console.log(`skipping associate for ${keyName}`) console.log(e) @@ -781,19 +823,8 @@ async function passProposal(proposalId, desposit="200000000usei", fees="200000u } else { await execute(`seid tx gov vote ${proposalId} yes --from ${from} -b sync -y --fees ${fees}`) } - // Poll for proposal status with shorter delay for faster tests - for(let i=0; i<200; i++) { - const proposal = await execute(`seid q gov proposal ${proposalId} -o json`) - const status = JSON.parse(proposal).status - if(status === "PROPOSAL_STATUS_PASSED") { - return proposalId - } - if(status === "PROPOSAL_STATUS_REJECTED" || status === "PROPOSAL_STATUS_FAILED") { - throw new Error(`Proposal ${proposalId} was rejected/failed with status: ${status}`) - } - await sleep(250) // Poll every 250ms instead of 1s for faster feedback - } - throw new Error("could not pass proposal "+proposalId) + await waitForProposalStatus(proposalId, "PROPOSAL_STATUS_PASSED") + return proposalId } async function registerPointerForERC20(erc20Address, fees="20000usei", from=adminKeyName) { @@ -1197,5 +1228,7 @@ module.exports = { waitForBaseFeeToEq, waitForBaseFeeToBeGt, waitForCondition, + waitForProposalStatus, getAccountSequence, + mineTransferBlock, }; diff --git a/docker/localnode/scripts/step4_config_override.sh b/docker/localnode/scripts/step4_config_override.sh index c3b35cc3d5..b8a2ad7b96 100755 --- a/docker/localnode/scripts/step4_config_override.sh +++ b/docker/localnode/scripts/step4_config_override.sh @@ -165,7 +165,6 @@ if [ "$AUTOBAHN" = "true" ]; then # Generate autobahn config using seid seid tendermint gen-autobahn-config $NODE_DIRS --output "$AUTOBAHN_CONFIG" - # Inject autobahn config file path into config.toml # Must be placed before any [section] header so TOML parser reads it as a top-level key. if grep -q "autobahn-config-file" ~/.sei/config/config.toml; then diff --git a/docker/localnode/scripts/step5_start_sei.sh b/docker/localnode/scripts/step5_start_sei.sh index bb02f08354..4474d768f5 100755 --- a/docker/localnode/scripts/step5_start_sei.sh +++ b/docker/localnode/scripts/step5_start_sei.sh @@ -9,5 +9,20 @@ mkdir -p $LOG_DIR echo "Starting the seid process for node $NODE_ID with invariant check interval=$INVARIANT_CHECK_INTERVAL..." seid start --chain-id sei --inv-check-period ${INVARIANT_CHECK_INTERVAL} > "$LOG_DIR/seid-$NODE_ID.log" 2>&1 & +SEID_PID=$! echo "Node $NODE_ID seid is started now" + +# launch.complete means the node's query surface is available, not merely that +# the process has started. The specific query here is arbitrary; any simple +# query would do. We use tendermint-validator-set because startup tests already +# rely on it and it exercises the CLI query path we need to be live. +until seid q tendermint-validator-set >/dev/null 2>&1 +do + if ! kill -0 "$SEID_PID" 2>/dev/null; then + echo "seid exited before becoming ready; see $LOG_DIR/seid-$NODE_ID.log" + exit 1 + fi + sleep 1 +done + echo "Done" >> build/generated/launch.complete diff --git a/docker/rpcnode/scripts/step1_configure_init.sh b/docker/rpcnode/scripts/step1_configure_init.sh index 61613709a8..8982628900 100755 --- a/docker/rpcnode/scripts/step1_configure_init.sh +++ b/docker/rpcnode/scripts/step1_configure_init.sh @@ -137,7 +137,6 @@ if [ "$AUTOBAHN" = "true" ]; then done seid tendermint gen-autobahn-config $NODE_DIRS --output "$AUTOBAHN_CONFIG" - # Inject autobahn-config-file as a top-level key in config.toml. It must # precede any [section] header so the TOML parser sees it at root scope. if grep -q "autobahn-config-file" ~/.sei/config/config.toml; then @@ -148,16 +147,26 @@ if [ "$AUTOBAHN" = "true" ]; then echo "Autobahn config written to $AUTOBAHN_CONFIG (fullnode via mode=full)" fi -# Override state sync configs +# Override state sync configs. Autobahn startup can legitimately sit at +# height 0 until a tx drives the first finalized block, so state sync +# cannot be configured yet in that case because trust-height 0 is invalid. +# Fall back to normal peer sync from genesis until the cluster has a real +# block history to trust. STATE_SYNC_RPC="192.168.10.10:26657" STATE_SYNC_PEER="2f9846450b7a3dcf4af1ac0082e3279c16744df8@172.31.9.18:26656,ec98c4a28a2023f4f976828c8a8e7127bfef4e1b@172.31.4.96:26656,b03014d67384fb0ef6ad992c77cefe4f9d2c1640@172.31.4.219:26656" curl "$STATE_SYNC_RPC"/net_info |jq -r '.peers[] | .url' |sed -e 's#mconn://##' >> build/generated/PEERS STATE_SYNC_PEER=$(paste -s -d ',' build/generated/PEERS) LATEST_HEIGHT=$(curl -s $STATE_SYNC_RPC/block | jq -r .block.header.height) -SYNC_BLOCK_HEIGHT=$LATEST_HEIGHT -SYNC_BLOCK_HASH=$(curl -s "$STATE_SYNC_RPC/block?height=$SYNC_BLOCK_HEIGHT" | jq -r .block_id.hash) -sed -i.bak -e "s|^enable *=.*|enable = true|" ~/.sei/config/config.toml -sed -i.bak -e "s|^rpc-servers *=.*|rpc-servers = \"$STATE_SYNC_RPC,$STATE_SYNC_RPC\"|" ~/.sei/config/config.toml -sed -i.bak -e "s|^trust-height *=.*|trust-height = $SYNC_BLOCK_HEIGHT|" ~/.sei/config/config.toml -sed -i.bak -e "s|^trust-hash *=.*|trust-hash = \"$SYNC_BLOCK_HASH\"|" ~/.sei/config/config.toml -sed -i.bak -e "s|^persistent-peers *=.*|persistent-peers = \"$STATE_SYNC_PEER\"|" ~/.sei/config/config.toml +if [ -n "$LATEST_HEIGHT" ] && [ "$LATEST_HEIGHT" -gt 0 ] 2>/dev/null; then + SYNC_BLOCK_HEIGHT=$LATEST_HEIGHT + SYNC_BLOCK_HASH=$(curl -s "$STATE_SYNC_RPC/block?height=$SYNC_BLOCK_HEIGHT" | jq -r .block_id.hash) + sed -i.bak -e "s|^enable *=.*|enable = true|" ~/.sei/config/config.toml + sed -i.bak -e "s|^rpc-servers *=.*|rpc-servers = \"$STATE_SYNC_RPC,$STATE_SYNC_RPC\"|" ~/.sei/config/config.toml + sed -i.bak -e "s|^trust-height *=.*|trust-height = $SYNC_BLOCK_HEIGHT|" ~/.sei/config/config.toml + sed -i.bak -e "s|^trust-hash *=.*|trust-hash = \"$SYNC_BLOCK_HASH\"|" ~/.sei/config/config.toml + sed -i.bak -e "s|^persistent-peers *=.*|persistent-peers = \"$STATE_SYNC_PEER\"|" ~/.sei/config/config.toml +else + echo "State sync unavailable at height ${LATEST_HEIGHT:-unknown}; falling back to peer sync." + sed -i.bak -e "s|^enable *=.*|enable = false|" ~/.sei/config/config.toml + sed -i.bak -e "s|^persistent-peers *=.*|persistent-peers = \"$STATE_SYNC_PEER\"|" ~/.sei/config/config.toml +fi diff --git a/evmrpc/block.go b/evmrpc/block.go index b6007edcfc..54cb2f2e78 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -150,17 +150,13 @@ func NewSei2BlockAPI( return blockAPI } -func (a *SeiBlockAPI) GetBlockByNumberExcludeTraceFail(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]interface{}, returnErr error) { - // Match eth_getBlockByNumber("0x0"): synthetic genesis, not the Tendermint block at height 0. - if number == 0 { - return encodeGenesisBlock(), nil - } +func (a *SeiBlockAPI) GetBlockByNumberExcludeTraceFail(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]any, returnErr error) { // Exclude synthetic txs (filterTransactions drops them) and ante-failure // stub receipts (EncodeTmBlock drops them via excludeUntraceable). return a.getBlockByNumber(ctx, number, fullTx, false, true) } -func (a *SeiBlockAPI) GetBlockByHashExcludeTraceFail(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { +func (a *SeiBlockAPI) GetBlockByHashExcludeTraceFail(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]any, returnErr error) { // See note on GetBlockByNumberExcludeTraceFail. return a.getBlockByHash(ctx, blockHash, fullTx, false, true) } @@ -185,7 +181,7 @@ func (a *BlockAPI) GetBlockTransactionCountByNumber(ctx context.Context, number if block == nil { return nil, nil } - if err = a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { + if err = a.watermarks.EnsureReceiptHeightAvailable(block.Block.Height); err != nil { return nil, err } return a.getEvmTxCount(block), nil @@ -207,18 +203,18 @@ func (a *BlockAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash if block == nil { return nil, nil } - if err = a.watermarks.EnsureReceiptHeightAvailable(ctx, block.Block.Height); err != nil { + if err = a.watermarks.EnsureReceiptHeightAvailable(block.Block.Height); err != nil { return nil, err } return a.getEvmTxCount(block), nil } -func (a *BlockAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { +func (a *BlockAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]any, returnErr error) { // used for both: eth_ and sei_ namespaces return a.getBlockByHash(ctx, blockHash, fullTx, a.includeShellReceipts, false) } -func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool, includeSyntheticTxs bool, excludeUntraceable bool) (result map[string]interface{}, returnErr error) { +func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool, includeSyntheticTxs bool, excludeUntraceable bool) (result map[string]any, returnErr error) { startTime := time.Now() defer func() { recordMetricsWithError(ctx, fmt.Sprintf("%s_getBlockByHash", a.namespace), a.connectionType, startTime, returnErr, recover()) @@ -250,15 +246,11 @@ func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fu return EncodeTmBlock(a.ctxProvider, a.txConfigProvider, block, a.keeper, fullTx, a.includeBankTransfers, includeSyntheticTxs, excludeUntraceable, a.globalBlockCache, a.cacheCreationMutex) } -func (a *BlockAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]interface{}, returnErr error) { +func (a *BlockAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]any, returnErr error) { startTime := time.Now() defer func() { recordMetricsWithError(ctx, fmt.Sprintf("%s_getBlockByNumber", a.namespace), a.connectionType, startTime, returnErr, recover()) }() - if number == 0 { - // for compatibility with the graph, always return genesis block - return encodeGenesisBlock(), nil - } return a.getBlockByNumber(ctx, number, fullTx, a.includeShellReceipts, false) } @@ -268,11 +260,15 @@ func (a *BlockAPI) getBlockByNumber( fullTx bool, includeSyntheticTxs bool, excludeUntraceable bool, -) (result map[string]interface{}, returnErr error) { +) (result map[string]any, returnErr error) { numberPtr, err := getBlockNumber(ctx, a.tmClient, number) if err != nil { return nil, err } + // synthetic genesis block, not the Tendermint block at height 0. + if number == 0 || (numberPtr == nil && a.ctxProvider(LatestCtxHeight).BlockHeight() == 0) { + return encodeGenesisBlock(), nil + } // Validate EVM block height for pacific-1 chain if numberPtr != nil { @@ -293,7 +289,7 @@ func (a *BlockAPI) getBlockByNumber( return EncodeTmBlock(a.ctxProvider, a.txConfigProvider, block, a.keeper, fullTx, a.includeBankTransfers, includeSyntheticTxs, excludeUntraceable, a.globalBlockCache, a.cacheCreationMutex) } -func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (result []map[string]interface{}, returnErr error) { +func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (result []map[string]any, returnErr error) { startTime := time.Now() defer func() { recordMetricsWithError(ctx, fmt.Sprintf("%s_getBlockReceipts", a.namespace), a.connectionType, startTime, returnErr, recover()) @@ -344,7 +340,7 @@ func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.Block // cannot spawn an unbounded number of goroutines. errgroup.SetLimit blocks // Go() until a slot frees, bounding the number of live goroutines rather // than just the number doing concurrent work. - allReceipts := make([]map[string]interface{}, len(txHashes)) + allReceipts := make([]map[string]any, len(txHashes)) g, ctx := errgroup.WithContext(ctx) g.SetLimit(maxBlockReceiptsConcurrency) for i, hash := range txHashes { @@ -375,7 +371,7 @@ func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.Block if err := g.Wait(); err != nil { return nil, err } - compactReceipts := make([]map[string]interface{}, 0) + compactReceipts := make([]map[string]any, 0) for _, r := range allReceipts { if len(r) > 0 { compactReceipts = append(compactReceipts, r) @@ -409,7 +405,7 @@ func EncodeTmBlock( excludeUntraceable bool, globalBlockCache BlockCache, cacheCreationMutex *sync.Mutex, -) (map[string]interface{}, error) { +) (map[string]any, error) { number := big.NewInt(block.Block.Height) blockhash := common.HexToHash(block.BlockID.Hash.String()) blockTime := block.Block.Time @@ -427,7 +423,7 @@ func EncodeTmBlock( } var blockGasUsed int64 chainConfig := types.DefaultChainConfig().EthereumConfig(k.ChainID(ctx)) - transactions := []interface{}{} + transactions := []any{} latestCtx := ctxProvider(LatestCtxHeight) msgs := filterTransactions(k, ctxProvider, txConfigProvider, block, includeSyntheticTxs, includeBankTransfers, cacheCreationMutex, globalBlockCache) @@ -535,7 +531,7 @@ func EncodeTmBlock( if cp := ctx.ConsensusParams(); cp != nil && cp.Block != nil { gasLimit = cp.Block.MaxGas } - result := map[string]interface{}{ + result := map[string]any{ "number": (*hexutil.Big)(number), "hash": blockhash, "parentHash": lastHash, diff --git a/evmrpc/block_test.go b/evmrpc/block_test.go index 6c83626e5c..a52c8201d5 100644 --- a/evmrpc/block_test.go +++ b/evmrpc/block_test.go @@ -75,7 +75,7 @@ func TestEncodeBankMsg(t *testing.T) { } res, err := evmrpc.EncodeTmBlock(func(i int64) sdk.Context { return ctx }, func(i int64) client.TxConfig { return TxConfig }, &resBlock, k, true, false, false, false, evmrpc.NewBlockCache(3000), &sync.Mutex{}) require.Nil(t, err) - txs := res["transactions"].([]interface{}) + txs := res["transactions"].([]any) require.Equal(t, 0, len(txs)) } @@ -115,7 +115,7 @@ func TestEncodeWasmExecuteMsg(t *testing.T) { } res, err := evmrpc.EncodeTmBlock(func(i int64) sdk.Context { return ctx }, func(i int64) client.TxConfig { return TxConfig }, &resBlock, k, true, false, true, false, evmrpc.NewBlockCache(3000), &sync.Mutex{}) require.Nil(t, err) - txs := res["transactions"].([]interface{}) + txs := res["transactions"].([]any) require.Equal(t, 1, len(txs)) ti := uint64(0) bh := common.HexToHash(MockBlockID.Hash.String()) @@ -163,7 +163,7 @@ func TestEncodeBankTransferMsg(t *testing.T) { } res, err := evmrpc.EncodeTmBlock(func(i int64) sdk.Context { return ctx }, func(i int64) client.TxConfig { return TxConfig }, &resBlock, k, true, true, false, false, evmrpc.NewBlockCache(3000), &sync.Mutex{}) require.Nil(t, err) - txs := res["transactions"].([]interface{}) + txs := res["transactions"].([]any) require.Equal(t, 1, len(txs)) bh := common.HexToHash(MockBlockID.Hash.String()) to := common.Address(toSeiAddr) @@ -218,7 +218,7 @@ func TestEncodeWasmExecuteMsg_GasUsedFromReceipt(t *testing.T) { res, err := evmrpc.EncodeTmBlock(func(i int64) sdk.Context { return ctx }, func(i int64) client.TxConfig { return TxConfig }, &resBlock, k, true, false, true, false, evmrpc.NewBlockCache(3000), &sync.Mutex{}) require.Nil(t, err) require.Equal(t, hexutil.Uint64(54321), res["gasUsed"]) - txs := res["transactions"].([]interface{}) + txs := res["transactions"].([]any) require.Equal(t, 1, len(txs)) } @@ -255,7 +255,7 @@ func TestEncodeBankTransferMsg_NoReceiptGasUsedZero(t *testing.T) { res, err := evmrpc.EncodeTmBlock(func(i int64) sdk.Context { return ctx }, func(i int64) client.TxConfig { return TxConfig }, &resBlock, k, true, true, false, false, evmrpc.NewBlockCache(3000), &sync.Mutex{}) require.Nil(t, err) require.Equal(t, hexutil.Uint64(0), res["gasUsed"]) - txs := res["transactions"].([]interface{}) + txs := res["transactions"].([]any) require.Equal(t, 1, len(txs)) } @@ -332,7 +332,7 @@ func TestEncodeTmBlock_ExcludeUntraceable(t *testing.T) { false /*fullTx*/, false /*includeBankTransfers*/, false /*includeSyntheticTxs*/, true, /*excludeUntraceable*/ evmrpc.NewBlockCache(3000), &sync.Mutex{}) require.NoError(t, err) - txs := res["transactions"].([]interface{}) + txs := res["transactions"].([]any) require.Len(t, txs, 1, "expected only the revert to survive, got %v", txs) require.Equal(t, strings.ToLower(TestNonPanicTxHash), strings.ToLower(txs[0].(string))) @@ -342,7 +342,7 @@ func TestEncodeTmBlock_ExcludeUntraceable(t *testing.T) { false /*fullTx*/, false /*includeBankTransfers*/, false /*includeSyntheticTxs*/, false, /*excludeUntraceable*/ evmrpc.NewBlockCache(3000), &sync.Mutex{}) require.NoError(t, err) - txs = res["transactions"].([]interface{}) + txs = res["transactions"].([]any) require.Len(t, txs, 2, "expected revert + ante stub to flow through, got %v", txs) } diff --git a/evmrpc/block_txcount_parity_test.go b/evmrpc/block_txcount_parity_test.go index c09ef88ad6..6f669135dd 100644 --- a/evmrpc/block_txcount_parity_test.go +++ b/evmrpc/block_txcount_parity_test.go @@ -18,7 +18,6 @@ import ( sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" tmbytes "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" - tmmock "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" @@ -29,7 +28,7 @@ const parityTestHeight int64 = 771 // Tendermint client stub for Block / BlockByHash / Status (count by number and by hash). type parityTxCountTMClient struct { - tmmock.Client + client.Client block *coretypes.ResultBlock } diff --git a/evmrpc/filter.go b/evmrpc/filter.go index 09baf1f2b0..69272c9374 100644 --- a/evmrpc/filter.go +++ b/evmrpc/filter.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "sort" "sync" "time" @@ -13,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" + gethlog "github.com/ethereum/go-ethereum/log" ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/hashicorp/golang-lru/v2/expirable" evmrpcconfig "github.com/sei-protocol/sei-chain/evmrpc/config" @@ -30,6 +32,11 @@ import ( var logger = seilog.NewLogger("evmrpc") +func init() { + handler := logger.Handler().WithAttrs([]slog.Attr{slog.String("logger", "evmrpc/geth")}) + gethlog.SetDefault(gethlog.NewLogger(handler)) +} + const ( // DB Concurrency Read Limit MaxDBReadConcurrency = 16 diff --git a/evmrpc/genesis_latest_state_test.go b/evmrpc/genesis_latest_state_test.go new file mode 100644 index 0000000000..9147bd031e --- /dev/null +++ b/evmrpc/genesis_latest_state_test.go @@ -0,0 +1,166 @@ +package evmrpc_test + +import ( + "context" + "net/url" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/sei-protocol/sei-chain/app" + "github.com/sei-protocol/sei-chain/app/legacyabci" + "github.com/sei-protocol/sei-chain/evmrpc" + "github.com/sei-protocol/sei-chain/sei-cosmos/client" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" + "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + evmstate "github.com/sei-protocol/sei-chain/x/evm/state" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" + "github.com/stretchr/testify/require" +) + +type freshChainClient struct { + client.Client +} + +func (*freshChainClient) EvmNextPendingNonce(common.Address) uint64 { + return 0 +} + +func (*freshChainClient) EvmTxByHash(common.Hash) (tmtypes.Tx, bool) { + return nil, false +} + +func (*freshChainClient) EvmProxy(common.Address) utils.Option[*url.URL] { + return utils.None[*url.URL]() +} + +func (*freshChainClient) Status(context.Context) (*coretypes.ResultStatus, error) { + return &coretypes.ResultStatus{ + SyncInfo: coretypes.SyncInfo{ + LatestBlockHeight: 0, + EarliestBlockHeight: 1, + }, + }, nil +} + +func (*freshChainClient) Genesis(context.Context) (*coretypes.ResultGenesis, error) { + return &coretypes.ResultGenesis{Genesis: &tmtypes.GenesisDoc{InitialHeight: 1}}, nil +} + +func (*freshChainClient) Block(context.Context, *int64) (*coretypes.ResultBlock, error) { + return nil, coretypes.ErrZeroOrNegativeHeight +} + +func (*freshChainClient) BlockByHash(context.Context, bytes.HexBytes) (*coretypes.ResultBlock, error) { + return &coretypes.ResultBlock{Block: &tmtypes.Block{Header: tmtypes.Header{Height: 0}}}, nil +} + +func latestLikeTags() []rpc.BlockNumber { + return []rpc.BlockNumber{ + rpc.LatestBlockNumber, + rpc.SafeBlockNumber, + rpc.FinalizedBlockNumber, + rpc.PendingBlockNumber, + } +} + +func TestStateAPILatestLikeTagsUseGenesisCommittedStateBeforeFirstCommit(t *testing.T) { + testApp := app.Setup(t, false, false, false) + checkCtx := testApp.GetCheckCtx() + require.Equal(t, int64(0), checkCtx.BlockHeight()) + _, address := testkeeper.MockAddressPair() + key := common.BytesToHash([]byte("key")) + value := common.BytesToHash([]byte("value")) + code := []byte{0xaa, 0xbb, 0xcc} + amount := sdk.NewInt(1234) + + coins := sdk.NewCoins(sdk.NewCoin(testApp.EvmKeeper.GetBaseDenom(checkCtx), amount)) + require.NoError(t, testApp.EvmKeeper.BankKeeper().MintCoins(checkCtx, evmtypes.ModuleName, coins)) + require.NoError(t, testApp.EvmKeeper.BankKeeper().SendCoinsFromModuleToAccount(checkCtx, evmtypes.ModuleName, sdk.AccAddress(address[:]), coins)) + testApp.EvmKeeper.SetCode(checkCtx, address, code) + testApp.EvmKeeper.SetState(checkCtx, address, key, value) + + ctxProvider := func(height int64) sdk.Context { + if height == evmrpc.LatestCtxHeight { + return testApp.GetCheckCtx() + } + queryCtx, err := testApp.CreateQueryContext(height, false) + require.NoError(t, err) + return queryCtx + } + tmClient := &freshChainClient{} + watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) + api := evmrpc.NewStateAPI(tmClient, &testApp.EvmKeeper, ctxProvider, evmrpc.ConnectionTypeHTTP, watermarks) + expectedBalance := evmstate.NewDBImpl(testApp.GetCheckCtx(), &testApp.EvmKeeper, true).GetBalance(address).ToBig().String() + + for _, tag := range latestLikeTags() { + blockRef := rpc.BlockNumberOrHashWithNumber(tag) + + balance, err := api.GetBalance(t.Context(), address, blockRef) + require.NoError(t, err) + require.Equal(t, expectedBalance, (*balance).ToInt().String()) + + gotCode, err := api.GetCode(t.Context(), address, blockRef) + require.NoError(t, err) + require.Equal(t, code, []byte(gotCode)) + + storage, err := api.GetStorageAt(t.Context(), address, key.Hex(), blockRef) + require.NoError(t, err) + require.Equal(t, value[:], []byte(storage)) + } +} + +func TestSimulationBackendLatestLikeTagsUseGenesisCommittedStateBeforeFirstCommit(t *testing.T) { + testApp := app.Setup(t, false, false, false) + checkCtx := testApp.GetCheckCtx() + require.Equal(t, int64(0), checkCtx.BlockHeight()) + _, address := testkeeper.MockAddressPair() + code := []byte{0xde, 0xad, 0xbe, 0xef} + amount := sdk.NewInt(4321) + + coins := sdk.NewCoins(sdk.NewCoin(testApp.EvmKeeper.GetBaseDenom(checkCtx), amount)) + require.NoError(t, testApp.EvmKeeper.BankKeeper().MintCoins(checkCtx, evmtypes.ModuleName, coins)) + require.NoError(t, testApp.EvmKeeper.BankKeeper().SendCoinsFromModuleToAccount(checkCtx, evmtypes.ModuleName, sdk.AccAddress(address[:]), coins)) + testApp.EvmKeeper.SetCode(checkCtx, address, code) + + tmClient := &freshChainClient{} + ctxProvider := func(height int64) sdk.Context { + if height == evmrpc.LatestCtxHeight { + return testApp.GetCheckCtx() + } + queryCtx, err := testApp.CreateQueryContext(height, false) + require.NoError(t, err) + return queryCtx + } + watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) + backend := evmrpc.NewBackend( + ctxProvider, + &testApp.EvmKeeper, + legacyabci.BeginBlockKeepers{}, + func(int64) client.TxConfig { return app.MakeEncodingConfig().TxConfig }, + tmClient, + &evmrpc.SimulateConfig{GasCap: 1_000_000, EVMTimeout: time.Second}, + testApp.BaseApp, + testApp.TracerAnteHandler, + evmrpc.NewBlockCache(16), + &sync.Mutex{}, + watermarks, + ) + expectedBalance := evmstate.NewDBImpl(testApp.GetCheckCtx(), &testApp.EvmKeeper, true).GetBalance(address).ToBig().String() + + for _, tag := range latestLikeTags() { + statedb, header, err := backend.StateAndHeaderByNumberOrHash(context.Background(), rpc.BlockNumberOrHashWithNumber(tag)) + require.NoError(t, err) + require.Equal(t, checkCtx.BlockHeight(), header.Number.Int64()) + require.Equal(t, code, statedb.GetCode(address)) + + db := evmstate.GetDBImpl(statedb) + require.Equal(t, expectedBalance, db.GetBalance(address).ToBig().String()) + } +} diff --git a/evmrpc/height_availability_test.go b/evmrpc/height_availability_test.go index 931e4cc652..bf1f24f891 100644 --- a/evmrpc/height_availability_test.go +++ b/evmrpc/height_availability_test.go @@ -14,7 +14,6 @@ import ( sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" @@ -24,7 +23,7 @@ import ( const highBlockHashHex = "0xfeedfeedfeedfeedfeedfeedfeedfeedfeedfeedfeedfeedfeedfeedfeedfeed" type heightTestClient struct { - mock.Client + client.Client highHash bytes.HexBytes highBlock *coretypes.ResultBlock earliest int64 @@ -45,7 +44,6 @@ func (*heightTestClient) EvmProxy(common.Address) utils.Option[*url.URL] { func newHeightTestClient(highHeight, earliest, latest int64) *heightTestClient { return &heightTestClient{ - Client: mock.Client{}, highHash: bytes.HexBytes(mustDecodeHex(highBlockHashHex[2:])), highBlock: &coretypes.ResultBlock{ Block: &tmtypes.Block{ @@ -87,6 +85,14 @@ func (c *heightTestClient) Status(context.Context) (*coretypes.ResultStatus, err }, nil } +func (c *heightTestClient) Genesis(context.Context) (*coretypes.ResultGenesis, error) { + return &coretypes.ResultGenesis{ + Genesis: &tmtypes.GenesisDoc{ + InitialHeight: c.earliest, + }, + }, nil +} + // blockNotFoundTestClient returns ResultBlock{Block: nil} for a specific hash to simulate Tendermint "block not found". type blockNotFoundTestClient struct { *heightTestClient @@ -110,7 +116,7 @@ func mustDecodeHex(h string) []byte { func testTxConfigProvider(int64) client.TxConfig { return nil } -func testCtxProvider(int64) sdk.Context { return sdk.Context{} } +func testCtxProvider(h int64) sdk.Context { return sdk.Context{}.WithBlockHeight(h) } // GetBlockByHash for a block whose height sits above safe latest must return // JSON null per the Ethereum JSON-RPC spec (the block doesn't exist from the @@ -269,7 +275,8 @@ func TestGetBlockReceiptsGenesisByNumber(t *testing.T) { func TestGetBlockByNumberExcludeTraceFailGenesis(t *testing.T) { t.Parallel() - api := NewSeiBlockAPI(nil, nil, testCtxProvider, testTxConfigProvider, ConnectionTypeHTTP, nil, nil, nil) + client := newHeightTestClient(1, 1, 1) + api := NewSeiBlockAPI(client, nil, testCtxProvider, testTxConfigProvider, ConnectionTypeHTTP, nil, nil, nil) block, err := api.GetBlockByNumberExcludeTraceFail(context.Background(), 0, false) require.NoError(t, err) require.NotNil(t, block) diff --git a/evmrpc/setup_test.go b/evmrpc/setup_test.go index 1f2c093a17..a442ed66c8 100644 --- a/evmrpc/setup_test.go +++ b/evmrpc/setup_test.go @@ -33,7 +33,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" tmutils "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" types2 "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" "github.com/sei-protocol/sei-chain/sei-tendermint/version" @@ -139,7 +138,7 @@ var NewHeadsCalled = make(chan struct{}, 1) var NotifierForTest = evmrpc.NewBlockHeaderNotifier(16) type MockClient struct { - mock.Client + client.Client latestOverride int64 } diff --git a/evmrpc/simulate.go b/evmrpc/simulate.go index 6315a1e51b..4ce24d6ba7 100644 --- a/evmrpc/simulate.go +++ b/evmrpc/simulate.go @@ -234,7 +234,7 @@ func (e *RevertError) ErrorCode() int { } // ErrorData returns the hex encoded revert reason. -func (e *RevertError) ErrorData() interface{} { +func (e *RevertError) ErrorData() any { return e.reason } @@ -313,22 +313,52 @@ func (b *Backend) SetTraceContextProvider(provider TraceContextProvider) { } } +func (b *Backend) isLatest(ctx context.Context, x rpc.BlockNumberOrHash) (bool, error) { + if x.BlockHash != nil { + return false, nil + } + if x.BlockNumber == nil { + return true, nil + } + resolved, err := getBlockNumber(ctx, b.tmClient, *x.BlockNumber) + return resolved == nil, err +} + func (b *Backend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (vm.StateDB, *ethtypes.Header, error) { - tmBlock, isLatestBlock, err := b.getBlockByNumberOrHash(ctx, blockNrOrHash) + sdkCtx := b.ctxProvider(LatestCtxHeight) + zeroExcessBlobGas := uint64(0) + header := ðtypes.Header{ + Difficulty: common.Big0, + Number: big.NewInt(sdkCtx.BlockHeight()), + BaseFee: b.keeper.GetNextBaseFeePerGas(sdkCtx).TruncateInt().BigInt(), + GasLimit: keeper.DefaultBlockGasLimit, + Time: toUint64(sdkCtx.BlockTime().Unix()), //nolint:gosec + ExcessBlobGas: &zeroExcessBlobGas, + } + isLatest, err := b.isLatest(ctx, blockNrOrHash) if err != nil { return nil, nil, err } - height := tmBlock.Block.Height - isWasmdCall, ok := ctx.Value(CtxIsWasmdPrecompileCallKey).(bool) - sdkCtx := b.ctxProvider(height).WithIsEVM(true).WithEVMEntryViaWasmdPrecompile(ok && isWasmdCall) - if !isLatestBlock { - // no need to check version for latest block - if err := CheckVersion(sdkCtx, b.keeper); err != nil { + if !isLatest || sdkCtx.BlockHeight() > 0 { + tmBlock, isLatest, err := b.getBlockByNumberOrHash(ctx, blockNrOrHash) + if err != nil { return nil, nil, err } + header.Number = big.NewInt(tmBlock.Block.Height) + header.Time = toUint64(tmBlock.Block.Time.Unix()) + header.ParentHash = common.BytesToHash(tmBlock.BlockID.Hash) + sdkCtx = b.ctxProvider(tmBlock.Block.Height) + if !isLatest { + if err := CheckVersion(sdkCtx, b.keeper); err != nil { + return nil, nil, err + } + } + } + isWasmdCall, ok := ctx.Value(CtxIsWasmdPrecompileCallKey).(bool) + sdkCtx = sdkCtx.WithIsEVM(true).WithEVMEntryViaWasmdPrecompile(ok && isWasmdCall) + if cp := sdkCtx.ConsensusParams(); cp != nil && cp.Block != nil { + header.GasLimit = uint64(cp.Block.MaxGas) //nolint:gosec } - header := b.getHeader(ctx, tmBlock) - header.BaseFee = b.keeper.GetNextBaseFeePerGas(b.ctxProvider(LatestCtxHeight)).TruncateInt().BigInt() return state.NewDBImpl(sdkCtx, b.keeper, true), header, nil } @@ -450,7 +480,7 @@ func (b Backend) BlockByNumber(ctx context.Context, bn rpc.BlockNumber) (*ethtyp }) } } - header := b.getHeader(ctx, tmBlock) + header := b.getHeader(tmBlock) block := ðtypes.Block{ Header_: header, Txs: txs, @@ -527,7 +557,7 @@ func (b *Backend) HeaderByNumber(ctx context.Context, bn rpc.BlockNumber) (*etht if err != nil { return nil, err } - return b.getHeader(ctx, tmBlock), nil + return b.getHeader(tmBlock), nil } func (b *Backend) StateAtTransaction(ctx context.Context, block *ethtypes.Block, txIndex int, reexec uint64) (*ethtypes.Transaction, vm.BlockContext, vm.StateDB, tracers.StateReleaseFunc, error) { @@ -631,7 +661,7 @@ func (b *Backend) StateAtBlock(ctx context.Context, block *ethtypes.Block, reexe func (b *Backend) initializeBlock(ctx context.Context, block *ethtypes.Block, ctxProvider TraceContextProvider) (sdk.Context, *coretypes.ResultBlock, tracers.StateReleaseFunc, error) { emptyRelease := func() {} // get the parent block using block.parentHash - prevBlockHeight := block.Number().Int64() - 1 + prevBlockHeight := max(block.Number().Int64()-1, 0) blockNumber := block.Number().Int64() tmBlock, err := blockByNumberRespectingWatermarks(ctx, b.tmClient, b.watermarks, &blockNumber, 1) @@ -676,7 +706,7 @@ func (b *Backend) CurrentHeader() *ethtypes.Header { ctx := context.Background() var header *ethtypes.Header if tmBlock, err := blockByNumberRespectingWatermarks(ctx, b.tmClient, b.watermarks, &height, 1); err == nil { - header = b.getHeader(ctx, tmBlock) + header = b.getHeader(tmBlock) } else { header = b.fallbackToEthHeaderOnly(height) } @@ -691,37 +721,26 @@ func (b *Backend) SuggestGasTipCap(context.Context) (*big.Int, error) { // getBlockByNumberOrHash resolves blockNrOrHash to a Tendermint ResultBlock in one RPC path // (by hash or by number, including latest). Callers pass the result to getHeader. func (b *Backend) getBlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*coretypes.ResultBlock, bool, error) { - var ( - block *coretypes.ResultBlock - err error - isLatestBlock bool - ) - if blockNrOrHash.BlockHash != nil { - block, err = blockByHashRespectingWatermarks(ctx, b.tmClient, b.watermarks, blockNrOrHash.BlockHash[:], 1) + block, err := blockByHashRespectingWatermarks(ctx, b.tmClient, b.watermarks, blockNrOrHash.BlockHash[:], 1) if err != nil { return nil, false, err } return block, false, nil } - var blockNumberPtr *int64 if blockNrOrHash.BlockNumber != nil { + var err error blockNumberPtr, err = getBlockNumber(ctx, b.tmClient, *blockNrOrHash.BlockNumber) if err != nil { return nil, false, err } - if blockNumberPtr == nil { - isLatestBlock = true - } - } else { - isLatestBlock = true } - block, err = blockByNumberRespectingWatermarks(ctx, b.tmClient, b.watermarks, blockNumberPtr, 1) + block, err := blockByNumberRespectingWatermarks(ctx, b.tmClient, b.watermarks, blockNumberPtr, 1) if err != nil { return nil, false, err } - return block, isLatestBlock, nil + return block, blockNumberPtr == nil, nil } // fallbackToEthHeaderOnly builds a minimal header when the block cannot be loaded @@ -737,7 +756,7 @@ func (b *Backend) fallbackToEthHeaderOnly(height int64) *ethtypes.Header { } } -func (b *Backend) getHeader(ctx context.Context, tmBlock *coretypes.ResultBlock) *ethtypes.Header { +func (b *Backend) getHeader(tmBlock *coretypes.ResultBlock) *ethtypes.Header { height := tmBlock.Block.Height zeroExcessBlobGas := uint64(0) baseFee := b.keeper.GetNextBaseFeePerGas(b.ctxProvider(height - 1)).TruncateInt().BigInt() diff --git a/evmrpc/simulate_test.go b/evmrpc/simulate_test.go index 831a7aa1d0..1d64eb507f 100644 --- a/evmrpc/simulate_test.go +++ b/evmrpc/simulate_test.go @@ -32,7 +32,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" tenderminttypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" @@ -45,15 +44,8 @@ import ( func primeReceiptStore(t *testing.T, store receipt.ReceiptStore, latest int64) { t.Helper() - if store == nil { - return - } - if latest <= 0 { - latest = 1 - } require.NoError(t, store.SetLatestVersion(latest)) require.NoError(t, store.SetEarliestVersion(1)) - require.Equal(t, int64(1), store.EarliestVersion()) } // bcAlwaysFailClient fails every Block call (header resolution uses a single block fetch). @@ -295,7 +287,7 @@ func TestCreateAccessList(t *testing.T) { result := resObj["result"].(map[string]any) require.Equal(t, []any{}, result["accessList"]) // the code uses MSTORE which does not trace access list - resObj = sendRequestBad(t, "createAccessList", txArgs, "latest") + resObj = sendRequestBad(t, "createAccessList", txArgs, "0x1") result = resObj["error"].(map[string]any) require.Equal(t, "error block", result["message"]) @@ -434,6 +426,7 @@ func TestPreV620UpgradeUsesBaseFeeNil(t *testing.T) { EVMTimeout: time.Second * 30, } + primeReceiptStore(t, testApp.EvmKeeper.ReceiptStore(), 3000) tmClient := NewMockClientWithLatest(3000) watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) backend := evmrpc.NewBackend( @@ -505,6 +498,7 @@ func TestGasLimitUsesConsensusOrConfig(t *testing.T) { ctxProvider := func(h int64) sdk.Context { return baseCtx.WithBlockHeight(h) } cfg := &evmrpc.SimulateConfig{GasCap: 10_000_000, EVMTimeout: time.Second} + primeReceiptStore(t, testApp.EvmKeeper.ReceiptStore(), 1) tmClient := &MockClient{} watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) backend := evmrpc.NewBackend(ctxProvider, &testApp.EvmKeeper, @@ -529,6 +523,7 @@ func TestGasLimitFallbackToDefault(t *testing.T) { // Case 1: ConsensusParams is nil → DefaultBlockGasLimit. nilParamsCtx := testApp.GetContextForDeliverTx([]byte{}).WithBlockHeight(1).WithConsensusParams(nil) ctxProvider1 := func(h int64) sdk.Context { return nilParamsCtx.WithBlockHeight(h) } + primeReceiptStore(t, testApp.EvmKeeper.ReceiptStore(), 1) tmClient1 := &MockClient{} watermarks1 := evmrpc.NewWatermarkManager(tmClient1, ctxProvider1, nil, testApp.EvmKeeper.ReceiptStore()) backend1 := evmrpc.NewBackend(ctxProvider1, &testApp.EvmKeeper, legacyabci.BeginBlockKeepers{}, func(int64) client.TxConfig { return TxConfig }, tmClient1, cfg, testApp.BaseApp, testApp.TracerAnteHandler, evmrpc.NewBlockCache(3000), &sync.Mutex{}, watermarks1) @@ -664,26 +659,35 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { tEnv := newTestEnv(t) // Test eth_call rate limiting with concurrent requests numRequests := 10 // Much more than the limit of 2 - results := make(chan error, numRequests) + runBurst := func() []error { + results := make(chan error, numRequests) + start := make(chan struct{}) + var wg sync.WaitGroup - // Start all requests concurrently to overwhelm the rate limiter - for i := 0; i < numRequests; i++ { - go func() { - _, err := tEnv.simAPI.Call(context.Background(), tEnv.args, nil, nil, nil) - results <- err - }() - } + // Release all requests at once to maximize contention on the limiter. + for range numRequests { + wg.Go(func() { + <-start + _, err := tEnv.simAPI.Call(t.Context(), tEnv.args, nil, nil, nil) + results <- err + }) + } - // Collect all results - var errors []error - for i := 0; i < numRequests; i++ { - errors = append(errors, <-results) + close(start) + wg.Wait() + close(results) + + var errors []error + for err := range results { + errors = append(errors, err) + } + return errors } // Count successful vs rejected requests successCount := 0 rejectedCount := 0 - for _, err := range errors { + for _, err := range runBurst() { if err == nil { successCount++ } else if strings.Contains(err.Error(), "eth_call rejected due to rate limit: server busy") { @@ -882,22 +886,18 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { // Start mixed requests and release them at once to maximize contention. for range numCallRequests { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start - _, err := tEnv.simAPI.Call(context.Background(), tEnv.args, nil, nil, nil) + _, err := tEnv.simAPI.Call(t.Context(), tEnv.args, nil, nil, nil) results <- err - }() + }) } for range numEstimateRequests { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start - _, err := tEnv.simAPI.EstimateGas(context.Background(), tEnv.args, nil, nil) + _, err := tEnv.simAPI.EstimateGas(t.Context(), tEnv.args, nil, nil) results <- err - }() + }) } close(start) @@ -952,23 +952,23 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { t.Run("TestRateLimitErrorFormat", func(t *testing.T) { tEnv := newTestEnv(t) // Test the error message format by overwhelming the rate limiter - numRequests := 20 + const numRequests = 20 results := make(chan error, numRequests) - - // Start requests concurrently to trigger rate limiting + start := make(chan struct{}) var wg sync.WaitGroup + + // Release all requests at once to reliably saturate the limiter. for range numRequests { - wg.Add(1) - go func() { - defer wg.Done() - _, err := tEnv.simAPI.Call(context.Background(), tEnv.args, nil, nil, nil) + wg.Go(func() { + <-start + _, err := tEnv.simAPI.Call(t.Context(), tEnv.args, nil, nil, nil) results <- err - }() + }) } + close(start) wg.Wait() close(results) - // Collect results and check error messages var rateLimitErrors []error for err := range results { if err != nil && strings.Contains(err.Error(), "rejected due to rate limit") { @@ -976,7 +976,6 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { } } - // Should have at least one rate limit error require.Greater(t, len(rateLimitErrors), 0, "Should have at least one rate limit error") // Verify error message format @@ -994,7 +993,7 @@ func TestSimulationAPIRequestLimiter(t *testing.T) { // pin a specific Block.Header / BlockID combination without dragging in the // rest of the mock infrastructure. type fixedBlockClient struct { - mock.Client + client.Client block *coretypes.ResultBlock } @@ -1043,6 +1042,7 @@ func TestTraceBlockByNumberUsesCompatDecoderForHistoricalCosmosTx(t *testing.T) testApp := app.Setup(t, false, false, false) ctx := testApp.GetContextForDeliverTx([]byte{}).WithBlockHeight(v65Height).WithClosestUpgradeName("v6.5") testApp.UpgradeKeeper.SetDone(ctx, "v6.5") + primeReceiptStore(t, testApp.EvmKeeper.ReceiptStore(), v65Height) ctxProvider := func(height int64) sdk.Context { if height == evmrpc.LatestCtxHeight { return ctx @@ -1181,6 +1181,7 @@ func TestBlockByNumberNonTracedTxPassesTxBytes(t *testing.T) { return ctx.WithBlockHeight(height) } + primeReceiptStore(t, testApp.EvmKeeper.ReceiptStore(), blockHeight) tmClient := &fixedBlockClient{block: tmBlock} watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, testApp.EvmKeeper.ReceiptStore()) backend := evmrpc.NewBackend( diff --git a/evmrpc/tests/mock_client.go b/evmrpc/tests/mock_client.go index 91a1a67b6c..4fb2b29414 100644 --- a/evmrpc/tests/mock_client.go +++ b/evmrpc/tests/mock_client.go @@ -18,14 +18,14 @@ import ( tmbytes "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" + rpcclient "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" seiutils "github.com/sei-protocol/sei-chain/utils" ) type MockClient struct { - mock.Client + rpcclient.Client blocks [][][]byte txResults [][]*abci.ExecTxResult consParamUpdates []*tmproto.ConsensusParams diff --git a/evmrpc/tx_test.go b/evmrpc/tx_test.go index 5670edaa0d..1e1800e12a 100644 --- a/evmrpc/tx_test.go +++ b/evmrpc/tx_test.go @@ -28,7 +28,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keyring" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" @@ -297,7 +296,7 @@ func TestGetTransactionReceiptExcludeTraceFailLateReceipt(t *testing.T) { // lowLatestTMClient reports a fixed LatestBlockHeight via Status, regardless // of what blocks the receipt store contains. type lowLatestTMClient struct { - mock.Client + client.Client latest int64 } diff --git a/evmrpc/watermark_manager.go b/evmrpc/watermark_manager.go index 0bbc8edfb4..756392b49f 100644 --- a/evmrpc/watermark_manager.go +++ b/evmrpc/watermark_manager.go @@ -46,6 +46,7 @@ func NewWatermarkManager( // Watermarks returns the earliest block height, earliest state height, and // latest height that are safe to serve. Earliest heights are inclusive. +// It is possible that block latest < block earliest, in case there are no blocks yet. func (m *WatermarkManager) Watermarks(ctx context.Context) (int64, int64, int64, error) { var ( latest int64 @@ -95,48 +96,28 @@ func (m *WatermarkManager) Watermarks(ctx context.Context) (int64, int64, int64, } if m.ctxProvider != nil { - if ctxHeight := m.ctxProvider(LatestCtxHeight).BlockHeight(); ctxHeight > 0 { - setLatest(ctxHeight) - } + setLatest(m.ctxProvider(LatestCtxHeight).BlockHeight()) } // State store heights (historical state DB) may lag behind block pruning. if ssLatest, ssEarliest, ok := m.fetchStateStoreWatermarks(); ok { - if ssLatest > 0 { - setLatest(ssLatest) - } - if ssEarliest > 0 { - setStateEarliest(ssEarliest) - } + setLatest(ssLatest) + setStateEarliest(ssEarliest) } // Receipt store version participates only in the latest watermark. if m.receiptStore != nil { - if latest := m.receiptStore.LatestVersion(); latest > 0 { - setLatest(latest) - } + setLatest(m.receiptStore.LatestVersion()) } if !latestSet { return 0, 0, 0, errNoHeightSource } - if !blockEarliestSet { - blockEarliest = 0 - } - if !stateEarliestSet { stateEarliest = blockEarliest } - if blockEarliest > latest { - return 0, 0, 0, fmt.Errorf("computed block earliest watermark %d is beyond latest %d", blockEarliest, latest) - } - - if stateEarliest > latest { - return 0, 0, 0, fmt.Errorf("computed state earliest watermark %d is beyond latest %d", stateEarliest, latest) - } - return blockEarliest, stateEarliest, latest, nil } @@ -178,7 +159,7 @@ func (m *WatermarkManager) ResolveHeight(ctx context.Context, blockNrOrHash rpc. return 0, err } height := res.Block.Height - if err := m.ensureWithinWatermarks(height, stateEarliest, latest); err != nil { + if err := ensureWithinWatermarks(height, stateEarliest, latest); err != nil { return 0, err } return height, nil @@ -203,7 +184,7 @@ func (m *WatermarkManager) ResolveHeight(ctx context.Context, blockNrOrHash rpc. if heightPtr == nil { return latest, nil } - if err := m.ensureWithinWatermarks(*heightPtr, stateEarliest, latest); err != nil { + if err := ensureWithinWatermarks(*heightPtr, stateEarliest, latest); err != nil { return 0, err } return *heightPtr, nil @@ -216,14 +197,14 @@ func (m *WatermarkManager) EnsureBlockHeightAvailable(ctx context.Context, heigh if err != nil { return err } - return m.ensureWithinWatermarks(height, blockEarliest, latest) + return ensureWithinWatermarks(height, blockEarliest, latest) } // EnsureReceiptHeightAvailable verifies that receipts for the given block height // have not been pruned from the receipt store. This is a separate check from // EnsureBlockHeightAvailable because the receipt store can be configured with a // smaller KeepRecent than the block or state stores. -func (m *WatermarkManager) EnsureReceiptHeightAvailable(_ context.Context, height int64) error { +func (m *WatermarkManager) EnsureReceiptHeightAvailable(height int64) error { if m.receiptStore == nil { return nil } @@ -234,7 +215,7 @@ func (m *WatermarkManager) EnsureReceiptHeightAvailable(_ context.Context, heigh return nil } -func (m *WatermarkManager) ensureWithinWatermarks(height, earliest, latest int64) error { +func ensureWithinWatermarks(height, earliest, latest int64) error { if height > latest { return fmt.Errorf("requested height %d is not yet available; safe latest is %d: %w", height, latest, ErrBlockHeightNotYetAvailable) } @@ -338,7 +319,9 @@ func (m *WatermarkManager) fetchTendermintWatermarks(ctx context.Context) (int64 return 0, 0, err } TraceTendermintIfApplicable(ctx, "Status", []string{}, status) - return status.SyncInfo.LatestBlockHeight, status.SyncInfo.EarliestBlockHeight, nil + latest := status.SyncInfo.LatestBlockHeight + earliest := status.SyncInfo.EarliestBlockHeight + return latest, earliest, nil } func (m *WatermarkManager) fetchStateStoreWatermarks() (int64, int64, bool) { diff --git a/evmrpc/watermark_manager_test.go b/evmrpc/watermark_manager_test.go index 0d6c3b6cf1..0541cd780c 100644 --- a/evmrpc/watermark_manager_test.go +++ b/evmrpc/watermark_manager_test.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/require" + "github.com/sei-protocol/sei-chain/sei-cosmos/client" storetypes "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-db/db_engine/types" @@ -21,7 +22,6 @@ import ( abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" @@ -114,27 +114,27 @@ func TestEnsureReceiptHeightAvailable(t *testing.T) { t.Run("no receipt store allows any height", func(t *testing.T) { wm := NewWatermarkManager(tmClient, nil, nil, nil) - require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 5)) + require.NoError(t, wm.EnsureReceiptHeightAvailable(5)) }) t.Run("receipt store with no pruning allows any height", func(t *testing.T) { rs := &fakeReceiptStore{latest: 200, earliest: 0} wm := NewWatermarkManager(tmClient, nil, nil, rs) - require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 5)) + require.NoError(t, wm.EnsureReceiptHeightAvailable(5)) }) t.Run("pruned receipt height returns error", func(t *testing.T) { rs := &fakeReceiptStore{latest: 200, earliest: 150} wm := NewWatermarkManager(tmClient, nil, nil, rs) - require.ErrorContains(t, wm.EnsureReceiptHeightAvailable(context.Background(), 100), "receipts have been pruned") - require.ErrorContains(t, wm.EnsureReceiptHeightAvailable(context.Background(), 149), "receipts have been pruned") + require.ErrorContains(t, wm.EnsureReceiptHeightAvailable(100), "receipts have been pruned") + require.ErrorContains(t, wm.EnsureReceiptHeightAvailable(149), "receipts have been pruned") }) t.Run("height within receipt retention succeeds", func(t *testing.T) { rs := &fakeReceiptStore{latest: 200, earliest: 150} wm := NewWatermarkManager(tmClient, nil, nil, rs) - require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 150)) - require.NoError(t, wm.EnsureReceiptHeightAvailable(context.Background(), 175)) + require.NoError(t, wm.EnsureReceiptHeightAvailable(150)) + require.NoError(t, wm.EnsureReceiptHeightAvailable(175)) }) } @@ -187,7 +187,7 @@ func TestStateWatermarksCanLagBlocks(t *testing.T) { } type fakeTMClient struct { - mock.Client + client.Client status *coretypes.ResultStatus statusErr error blockByHash *coretypes.ResultBlock diff --git a/giga/deps/testutil/processblock/common.go b/giga/deps/testutil/processblock/common.go index c2d6ad97f2..13c7603274 100644 --- a/giga/deps/testutil/processblock/common.go +++ b/giga/deps/testutil/processblock/common.go @@ -48,7 +48,7 @@ func NewTestApp(t *testing.T) *App { if err != nil { panic(err) } - _, err = a.InitChain(context.Background(), &types.RequestInitChain{ + _, err = a.InitChain(&types.RequestInitChain{ Time: time.Now(), ChainId: "tendermint_test", ConsensusParams: &cp, diff --git a/integration_test/autobahn/autobahn_test.go b/integration_test/autobahn/autobahn_test.go index 906b3e4d22..2857ebde36 100644 --- a/integration_test/autobahn/autobahn_test.go +++ b/integration_test/autobahn/autobahn_test.go @@ -26,6 +26,7 @@ import ( tmjson "github.com/sei-protocol/sei-chain/sei-tendermint/libs/json" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" + "github.com/sei-protocol/sei-chain/testutil/evmtest" ) const ( @@ -59,26 +60,21 @@ const ( // checks (the rpc-node's 8545 isn't host-published). evmRPCURLOnContainerLocalhost = "http://localhost:8545" - // heightPoll governs both waitForStableHeight and waitForHeightAdvance: - // the fullnode's read of /abci_info trails the cluster while - // runExecute drains buffered blocks, and a killed-peer failover - // (DialInterval-bounded, ~10s) holds height static for that long. - // Polling lets each test absorb whatever combination of those delays - // actually applies, instead of guessing a sleep duration. - // Bounds are 2× the expected failover+drain so a slow CI runner has - // headroom without giving up another whole minute on every run. + // heightPoll governs waitForStableHeight: the fullnode's read of + // /abci_info trails the cluster while runExecute drains buffered + // blocks, and a killed-peer failover (DialInterval-bounded, ~10s) + // holds height static for that long. Polling lets each test absorb + // whatever combination of those delays actually applies, instead of + // guessing a sleep duration. heightPoll = 1 * time.Second haltStableWindow = 20 * time.Second - // Initial fullnode sync can pause at a height briefly while Autobahn - // streams settle; require eventual progress rather than one fixed sample. - blockProductionAdvanceMax = 30 * time.Second // 2m / 90s give headroom for the fullnode catch-up backlog the // preceding subtest may have left (failover delay during // LivenessUnderMaxFaults can put the fullnode ~600 blocks behind, // which takes ~60s to drain on top of the halt-detection window). // CI runners are slower than local; 1m was tight enough to flake. haltStableTimeout = 2 * time.Minute - heightAdvanceMax = 90 * time.Second + testRecipientEVM = "0x1000000000000000000000000000000000000001" ) var ( @@ -107,10 +103,8 @@ func listRunningNodes(t *testing.T) []string { return strings.Fields(strings.TrimSpace(string(out))) } -// getHeight reads last_block_height from /abci_info and retries until the -// chain has produced at least one block (height > 0). ABCI returns 0 between -// InitChain and the first FinalizeBlock; we treat that as "not ready" since -// all callers assume a live, advancing chain. +// getHeight reads last_block_height from /abci_info and retries until a +// non-zero committed height is observed. // // Uses abci_info instead of /status because /status reads from the CometBFT // block store, which autobahn does not populate. @@ -128,6 +122,15 @@ func getHeight(t *testing.T) int64 { return 0 } +func currentHeight(t *testing.T) int64 { + t.Helper() + h, err := fetchHeight() + if err != nil { + t.Fatalf("fetch height: %v", err) + } + return h +} + // waitForStableHeight polls getHeight every heightPoll. It returns the // height once the value has stayed constant for at least `window`. Useful // after killing validators: cluster halt is observable through the rpc- @@ -155,26 +158,6 @@ func waitForStableHeight(t *testing.T, window, timeout time.Duration) int64 { return 0 } -// waitForHeightAdvance polls getHeight every heightPoll, returning the -// first observed value strictly greater than `base`. Used by liveness -// tests where progress is expected but may be delayed by the fullnode's -// block-sync failover from a killed peer. Fails the test on timeout. -func waitForHeightAdvance(t *testing.T, base int64, timeout time.Duration) int64 { - t.Helper() - deadline := time.Now().Add(timeout) - last := base - for time.Now().Before(deadline) { - h := getHeight(t) - if h > base { - return h - } - last = h - time.Sleep(heightPoll) - } - t.Fatalf("height did not advance past %d within %s (last seen %d)", base, timeout, last) - return 0 -} - func fetchHeight() (int64, error) { resp, err := heightClient.Get(abciInfoURL) if err != nil { @@ -233,6 +216,109 @@ func dockerExecAllowFail(container, script string) { _ = exec.Command("docker", "exec", container, "sh", "-c", script).Run() } +func waitForReceiptBlockNumber(t *testing.T, txHash string) int64 { + t.Helper() + for { + if err := t.Context().Err(); err != nil { + t.Fatalf("receipt for %s not observed before test context ended: %v", txHash, err) + } + resp, err := evmRPCInContainer(fullnodeContainer, "eth_getTransactionReceipt", []any{txHash}) + if err != nil { + time.Sleep(heightPoll) + continue + } + if resp.Error != nil { + t.Fatalf("eth_getTransactionReceipt(%s): code=%d message=%s", txHash, resp.Error.Code, resp.Error.Message) + } + if string(resp.Result) == "null" || len(resp.Result) == 0 { + time.Sleep(heightPoll) + continue + } + + var receipt struct { + BlockNumber string `json:"blockNumber"` + Status string `json:"status"` + } + if err := json.Unmarshal(resp.Result, &receipt); err != nil { + t.Fatalf("decode receipt for %s: %v\nbody: %s", txHash, err, resp.Result) + } + if receipt.Status != "" && receipt.Status != "0x1" { + t.Fatalf("tx %s reverted with status %s", txHash, receipt.Status) + } + if receipt.BlockNumber == "" { + time.Sleep(heightPoll) + continue + } + + var height int64 + if _, err := fmt.Sscanf(receipt.BlockNumber, "0x%x", &height); err != nil { + t.Fatalf("parse receipt block number %q for %s: %v", receipt.BlockNumber, txHash, err) + } + return height + } +} + +func evmBalanceHex(t *testing.T, address string) string { + t.Helper() + resp, err := evmRPCInContainer(fullnodeContainer, "eth_getBalance", []any{address, "latest"}) + if err != nil { + t.Fatalf("eth_getBalance(%s): %v", address, err) + } + if resp.Error != nil { + t.Fatalf("eth_getBalance(%s): code=%d message=%s", address, resp.Error.Code, resp.Error.Message) + } + var balance string + if err := json.Unmarshal(resp.Result, &balance); err != nil { + t.Fatalf("decode balance for %s: %v\nbody: %s", address, err, resp.Result) + } + return balance +} + +func sendEvmTx(t *testing.T, container string) string { + t.Helper() + // Progress-only tx: these subtests use "a tx finalized in a new block" as + // the observable signal that Autobahn is live or halted. + txHash, err := evmtest.SendTinyEvmTx(t.Context(), evmtest.DockerTxConfig{ + Container: container, + Password: "12345678", + From: "node_admin", + Recipient: testRecipientEVM, + ChainID: "sei", + EVMRPCURL: evmRPCURLOnContainerLocalhost, + }) + if err != nil { + t.Fatalf("send evm tx: %v", err) + } + return txHash +} + +func sendEvmTxAndWait(t *testing.T, container string) int64 { + t.Helper() + baseHeight := currentHeight(t) + txHash := sendEvmTx(t, container) + receiptHeight := waitForReceiptBlockNumber(t, txHash) + if receiptHeight <= baseHeight { + t.Fatalf("expected tx %s to land after height %d, got receipt at %d", txHash, baseHeight, receiptHeight) + } + return receiptHeight +} + +func sendEvmTxExpectNoInclusion(t *testing.T, container string, baseHeight int64) { + t.Helper() + // Progress-only tx: after quorum loss, this should remain uncommitted and + // height should stay fixed, proving that no new block can be produced. + txHash := sendEvmTx(t, container) + hAfter := waitForStableHeight(t, haltStableWindow, haltStableTimeout) + if hAfter != baseHeight { + t.Fatalf("expected no inclusion after quorum loss, but height advanced from %d to %d", baseHeight, hAfter) + } + resp, err := evmRPCInContainer(fullnodeContainer, "eth_getTransactionReceipt", []any{txHash}) + if err == nil && resp != nil && resp.Error == nil && string(resp.Result) != "null" && len(resp.Result) > 0 { + t.Fatalf("expected no inclusion after quorum loss, but tx %s received receipt %s", txHash, resp.Result) + } + t.Logf("height stayed at %d after submitted tx", hAfter) +} + // TestMain brings up the autobahn docker cluster before the test runs and // tears it down afterward. The working directory is changed to the repo root // so the `make docker-cluster-*` targets resolve their relative paths. @@ -480,7 +566,7 @@ func TestAutobahn(t *testing.T) { t.Logf("cluster size = %d, max tolerated faults = %d (assuming equal weights)", clusterSize, maxFaults) t.Run("BlockProduction", testBlockProduction) - t.Run("BankTransfer", testBankTransfer) + t.Run("EVMTransfer", testEVMTransfer) t.Run("LivenessUnderMaxFaults", testLivenessUnderMaxFaults) t.Run("HaltsBeyondMaxFaults", testHaltsBeyondMaxFaults) t.Run("Recovery", testRecovery) @@ -537,9 +623,12 @@ func testRecovery(t *testing.T) { target := clusterSize - 1 - maxFaults restartNode(t, target) - // Poll for the chain to advance. Give the restarted seid time to init - // and rejoin consensus, plus any fullnode failover. - hAfter := waitForHeightAdvance(t, hBefore, heightAdvanceMax) + // A committed tx is the liveness signal here: once quorum is restored, + // a new tx should finalize and advance height. + hAfter := sendEvmTxAndWait(t, "sei-node-0") + if hAfter <= hBefore { + t.Fatalf("expected committed tx after recovery to advance height past %d, got %d", hBefore, hAfter) + } t.Logf("height after restart: %d", hAfter) // assertAutobahnEnabled greps every running container's log. The restarted @@ -551,17 +640,15 @@ func testRecovery(t *testing.T) { func testBlockProduction(t *testing.T) { assertAutobahnEnabled(t) - h1 := getHeight(t) - t.Logf("height: %d", h1) - h2 := waitForHeightAdvance(t, h1, blockProductionAdvanceMax) - t.Logf("height after progress: %d", h2) + h := sendEvmTxAndWait(t, "sei-node-0") + t.Logf("height after committed evm tx: %d", h) - // Verify the Autobahn-routed tmRPC handlers serve real data at h2 (a + // Verify the Autobahn-routed tmRPC handlers serve real data at h (a // recently committed height — past tail of the chain, so historical // query paths are exercised without racing the producer). Each // endpoint asserts one observable property; a single mismatch fails // the test with the specific shape that broke. - assertTmRPCEndpoints(t, h2) + assertTmRPCEndpoints(t, h) } // assertTmRPCEndpoints exercises the tmRPC surface that PR #3310 wires up @@ -663,50 +750,15 @@ func fetchTmRPC[T any](t *testing.T, url string, into *T) { } } -func testBankTransfer(t *testing.T) { +func testEVMTransfer(t *testing.T) { assertAutobahnEnabled(t) - - // Create recipient. stderr is redirected inside the container so stdout is pure JSON. - createOut := dockerExec(t, "sei-node-0", - "printf '12345678\n12345678\n' | seid keys add test_recipient --output json 2>/dev/null") - var key struct { - Address string `json:"address"` - } - if err := json.Unmarshal([]byte(strings.TrimSpace(createOut)), &key); err != nil { - t.Fatalf("parse recipient address: %v\noutput: %s", err, createOut) - } - t.Logf("recipient: %s", key.Address) - - // Send from node_admin (genesis account) to recipient. - // Use -b sync (not -b block) because CometBFT consensus is disabled in autobahn mode. - // TODO: support -b block once autobahn supports it. - sendCmd := fmt.Sprintf( - "printf '12345678\n' | seid tx bank send node_admin %s 1000000usei "+ - "--chain-id sei --fees 2000usei -b sync -y --output json", - key.Address) - dockerExec(t, "sei-node-0", sendCmd) - - // Poll for balance. Tolerate transient query failures before the tx finalizes. - t.Log("waiting for tx to finalize...") - queryCmd := fmt.Sprintf("seid q bank balances %s --denom usei --output json 2>/dev/null", key.Address) - var balance string - for attempt := 0; attempt < 15; attempt++ { - out, _ := exec.Command("docker", "exec", "sei-node-0", "sh", "-c", queryCmd).Output() - var b struct { - Amount string `json:"amount"` - } - if err := json.Unmarshal([]byte(strings.TrimSpace(string(out))), &b); err == nil { - balance = b.Amount - if balance == "1000000" { - break - } - } - time.Sleep(2 * time.Second) - } - t.Logf("balance: %s usei", balance) - if balance != "1000000" { - t.Fatalf("expected balance 1000000, got %s", balance) + before := evmBalanceHex(t, testRecipientEVM) + h := sendEvmTxAndWait(t, "sei-node-0") + after := evmBalanceHex(t, testRecipientEVM) + if before == after { + t.Fatalf("expected recipient %s balance to change after evm tx at height %d", testRecipientEVM, h) } + t.Logf("evm transfer committed at height %d (balance %s -> %s)", h, before, after) } // killNode kills seid inside sei-node- via pkill. Tolerates non-zero exit @@ -732,7 +784,10 @@ func testLivenessUnderMaxFaults(t *testing.T) { for i := 0; i < maxFaults; i++ { killNode(t, clusterSize-1-i) } - hAfter := waitForHeightAdvance(t, hBefore, heightAdvanceMax) + hAfter := sendEvmTxAndWait(t, "sei-node-0") + if hAfter <= hBefore { + t.Fatalf("expected committed tx with %d faults to advance height past %d, got %d", maxFaults, hBefore, hAfter) + } t.Logf("height after: %d", hAfter) } @@ -751,12 +806,5 @@ func testHaltsBeyondMaxFaults(t *testing.T) { killNode(t, clusterSize-1-maxFaults) hBefore := waitForStableHeight(t, haltStableWindow, haltStableTimeout) t.Logf("height: %d (expecting halt)", hBefore) - // waitForStableHeight already returned only after haltStableWindow of - // no movement; the sample we just took is the halted height. - hAfter := getHeight(t) - t.Logf("height after stability: %d", hAfter) - if hAfter != hBefore { - t.Fatalf("chain should halt with %d/%d validators (height changed: %d -> %d)", - clusterSize-maxFaults-1, clusterSize, hBefore, hAfter) - } + sendEvmTxExpectNoInclusion(t, "sei-node-0", hBefore) } diff --git a/integration_test/contracts/create_tokenfactory_denoms.sh b/integration_test/contracts/create_tokenfactory_denoms.sh index b90b20d7d2..dc036224be 100755 --- a/integration_test/contracts/create_tokenfactory_denoms.sh +++ b/integration_test/contracts/create_tokenfactory_denoms.sh @@ -1,8 +1,8 @@ #!/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 '"') +keyname=admin +keyaddress=$(printf "12345678\n" | $seidbin keys show "$keyname" -a 2>/dev/null) chainid=$($seidbin status | jq ".NodeInfo.network" | tr -d '"') seihome=$(git rev-parse --show-toplevel | tr -d '"') @@ -11,7 +11,12 @@ source "$(dirname "$0")/../utils/_tx_helpers.sh" cd $seihome || exit echo "Deploying first set of tokenfactory denoms..." -beginning_block_height=$($seidbin status | jq -r '.SyncInfo.latest_block_height') +# Capture a committed baseline height before the first denom creation. This +# script later records and compares state around "beginning block height", so +# under allow_empty_blocks=false we must force one real block and remember the +# exact inclusion height instead of sampling status at height 0 / an idle tip. +bootstrap_block_height=$(bank_send_and_get_height "$keyname" "$keyaddress" "1usei") || exit 1 +beginning_block_height="$bootstrap_block_height" echo "$beginning_block_height" > $seihome/integration_test/contracts/tfk_beginning_block_height.txt echo "$keyaddress" > $seihome/integration_test/contracts/tfk_creator_id.txt diff --git a/integration_test/contracts/deploy_dex_contract.sh b/integration_test/contracts/deploy_dex_contract.sh index 2efc42ce3c..9d0236bc16 100755 --- a/integration_test/contracts/deploy_dex_contract.sh +++ b/integration_test/contracts/deploy_dex_contract.sh @@ -1,7 +1,7 @@ #!/bin/bash seidbin=$(which ~/go/bin/seid | tr -d '"') -keyname=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"') +keyname=admin chainid=$($seidbin status | jq ".NodeInfo.network" | tr -d '"') seihome=$(git rev-parse --show-toplevel | tr -d '"') contract_name=$1 diff --git a/integration_test/contracts/deploy_timelocked_token_contract.sh b/integration_test/contracts/deploy_timelocked_token_contract.sh index ff1b38e4f4..efc4e7c570 100755 --- a/integration_test/contracts/deploy_timelocked_token_contract.sh +++ b/integration_test/contracts/deploy_timelocked_token_contract.sh @@ -1,7 +1,7 @@ #!/bin/bash seidbin=$(which ~/go/bin/seid | tr -d '"') -keyname=$(printf "12345678\n" | $seidbin keys list --output json | jq ".[0].name" | tr -d '"') +keyname=admin 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..038a8c6a0a 100755 --- a/integration_test/contracts/deploy_wasm_contracts.sh +++ b/integration_test/contracts/deploy_wasm_contracts.sh @@ -1,8 +1,8 @@ #!/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 '"') +keyname=admin +keyaddress=$(printf "12345678\n" | $seidbin keys show "$keyname" -a 2>/dev/null) chainid=$($seidbin status | jq ".NodeInfo.network" | tr -d '"') seihome=$(git rev-parse --show-toplevel | tr -d '"') @@ -11,7 +11,12 @@ source "$(dirname "$0")/../utils/_tx_helpers.sh" cd $seihome || exit echo "Deploying first set of contracts..." -beginning_block_height=$($seidbin status | jq -r '.SyncInfo.latest_block_height') +# Capture a committed baseline height before storing the first wasm contracts. +# The follow-up checks use this "beginning block height" as the last state +# before any contract writes, so under allow_empty_blocks=false we force one +# real block and persist its exact inclusion height. +bootstrap_block_height=$(bank_send_and_get_height "$keyname" "$keyaddress" "1usei") || exit 1 +beginning_block_height="$bootstrap_block_height" echo "$beginning_block_height" > $seihome/integration_test/contracts/wasm_beginning_block_height.txt echo "$keyaddress" > $seihome/integration_test/contracts/wasm_creator_id.txt diff --git a/integration_test/contracts/verify_flatkv_evm_migrate.sh b/integration_test/contracts/verify_flatkv_evm_migrate.sh index 071a7070cb..8c02557d29 100755 --- a/integration_test/contracts/verify_flatkv_evm_migrate.sh +++ b/integration_test/contracts/verify_flatkv_evm_migrate.sh @@ -758,24 +758,13 @@ EOF" done echo "Voted yes on proposal $proposal_id from a quorum of validators; waiting for it to pass..." - # Poll for PASSED rather than a fixed sleep so a slow voting period does - # not flake the test (and a rejection fails fast). - local elapsed=0 - local status="" - while [ "$elapsed" -lt "$GOV_PASS_TIMEOUT" ]; do - status=$(docker exec "sei-node-0" build/seid q gov proposal "$proposal_id" --output json 2>/dev/null \ - | jq -r '.status // ""' 2>/dev/null || true) - if [ "$status" = "PROPOSAL_STATUS_PASSED" ]; then - break - fi - if [ "$status" = "PROPOSAL_STATUS_REJECTED" ] || [ "$status" = "PROPOSAL_STATUS_FAILED" ]; then - echo "ERROR: migration param-change proposal $proposal_id ended in status $status" >&2 - exit 1 - fi - echo "Waiting for proposal $proposal_id to pass (elapsed=${elapsed}s/${GOV_PASS_TIMEOUT}s, status=${status:-unknown})" - sleep 5 - elapsed=$((elapsed + 5)) - done + local status + status=$(docker exec "sei-node-0" bash -lc " + cd /sei-protocol/sei-chain + seidbin=build/seid chainid=sei + source integration_test/utils/_tx_helpers.sh + wait_for_proposal_status '$proposal_id' PROPOSAL_STATUS_PASSED admin '$GOV_PASS_TIMEOUT' + " 2>/dev/null || true) if [ "$status" != "PROPOSAL_STATUS_PASSED" ]; then echo "ERROR: migration param-change proposal $proposal_id did not pass within ${GOV_PASS_TIMEOUT}s (last status=$status)" >&2 exit 1 diff --git a/integration_test/evm_module/scripts/evm_rpc_tests.sh b/integration_test/evm_module/scripts/evm_rpc_tests.sh index f73c66418b..d04211e9a7 100755 --- a/integration_test/evm_module/scripts/evm_rpc_tests.sh +++ b/integration_test/evm_module/scripts/evm_rpc_tests.sh @@ -22,42 +22,84 @@ run() { 'export PATH=$PATH:/root/go/bin && printf "%s\n" "$SEI_EVM_IO_PASSWORD" | "$@"' bash "$@" } -# Resolve sender's cosmos address once. On any failure (transient -# docker hiccup, container start race, missing key) FROM_ADDR stays -# empty and the wait helper below becomes a no-op — the script falls -# back to the original unprotected behavior rather than crashing -# under set -e. -FROM_ADDR=$(run seid keys show "$FROM" "${KEYRING_ARGS[@]}" -a 2>/dev/null) || FROM_ADDR= - -# Cosmos account sequence for $FROM_ADDR (or empty on any error). -# Always exits 0 so the calling `local cur=$(get_from_seq)` doesn't -# trip set -e in command substitution. -get_from_seq() { - if [ -z "$FROM_ADDR" ]; then echo; return 0; fi - local s - s=$(run seid q account "$FROM_ADDR" -o json 2>/dev/null | jq -r '.sequence // ""' 2>/dev/null) || true - echo "$s" +get_latest_height() { + local h + h=$(run seid status 2>/dev/null | jq -r '.SyncInfo.latest_block_height // "0"' 2>/dev/null) || true + echo "${h:-0}" } -# Wait until $FROM_ADDR's sequence advances past $1, with a 5s timeout. -# Direct causal "previous tx committed" signal: the sender's sequence -# advances atomically when its tx is included in a block, so by the -# time this returns the next CLI's pre-flight `q account` will read -# the post-tx sequence. No-op if FROM_ADDR resolution failed. -wait_from_seq_advance() { +wait_until_height_exceeds() { local prev="$1" - if [ -z "$FROM_ADDR" ] || [ -z "$prev" ]; then return 0; fi - local deadline=$(($(date +%s) + 5)) + local deadline=$(($(date +%s) + 30)) while [ "$(date +%s)" -lt "$deadline" ]; do - local cur; cur=$(get_from_seq) + local cur; cur=$(get_latest_height) if [[ "$cur" =~ ^[0-9]+$ ]] && [ "$cur" -gt "$prev" ]; then return 0 fi sleep 0.5 done + echo "timed out waiting for height to exceed ${prev}" >&2 + return 1 +} + +wait_for_receipt_field() { + local tx_hash="$1" + local field="$2" + local deadline=$(($(date +%s) + 30)) + while [ "$(date +%s)" -lt "$deadline" ]; do + local resp + resp=$(docker exec "$CONTAINER" curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$tx_hash\"]}" "$EVM_RPC_URL" 2>/dev/null) || true + local value + value=$(echo "$resp" | jq -r --arg field "$field" '.result[$field] // empty' 2>/dev/null) || true + if [[ -n "$value" && "$value" != "null" ]]; then + echo "$value" + return 0 + fi + sleep 1 + done + echo "timed out waiting for executed receipt field ${field} for ${tx_hash}" >&2 + return 1 +} + +bump_chain_to_height() { + local target="$1" + local height; height=$(get_latest_height) + if ! [[ "$height" =~ ^[0-9]+$ ]]; then + echo "failed to read numeric chain height before preseeding: ${height}" >&2 + return 1 + fi + local max_iters=$((target - height + 5)) + if [ "$max_iters" -lt 1 ]; then + return 0 + fi + local iter=0 + while [ "$height" -lt "$target" ]; do + iter=$((iter + 1)) + if [ "$iter" -gt "$max_iters" ]; then + echo "timed out preseeding historical blocks: height=${height}, target=${target}, attempts=${iter}" >&2 + return 1 + fi + # Progress-only EVM send: these fixtures need real historical blocks first. + local tx_hash + tx_hash=$(run seid tx evm send "$RECIPIENT" 1 --from "$FROM" "${KEYRING_ARGS[@]}" --chain-id sei --evm-rpc "$EVM_RPC_URL" -b sync -y | grep -oE '0x[a-fA-F0-9]{64}' | head -1) + if [[ -z "$tx_hash" ]]; then + echo "failed to extract progress tx hash while preseeding historical blocks" >&2 + return 1 + fi + local receipt_height + if ! receipt_height=$(wait_for_receipt_field "$tx_hash" blockNumber); then + return 1 + fi + height=$((receipt_height)) + done } # Seed chain with block/tx/contract; export SEI_EVM_IO_SEED_BLOCK so .iox __SEED__ tag resolves to deploy block. +# Static .io fixtures contain hard-coded historical block numbers up to 0x2d. +# Under Autobahn without empty blocks, create enough real blocks first so those +# requests stay within the available history. +bump_chain_to_height 100 + # CLI deploy expects hex file with no whitespace; write trimmed hex to a temp path in the container. docker exec "$CONTAINER" /bin/bash -c "tr -d '[:space:]' < \"$CONTRACT_HEX\" > /tmp/minimal_contract.hex" # Use -b sync (not -b block): under Autobahn the cosmos KV indexer that @@ -66,35 +108,20 @@ docker exec "$CONTAINER" /bin/bash -c "tr -d '[:space:]' < \"$CONTRACT_HEX\" > / # downstream eth_getTransactionReceipt polling handles inclusion # confirmation independently. # -# Poll the sender's sequence between each pair of consecutive -b sync -# submissions so the next CLI's pre-flight `q account` doesn't race -# the mempool's still-pending prior tx (otherwise both sign with the -# same sequence and the second's CheckTx rejects with "incorrect -# account sequence"). For the send line that's required because it -# has no `|| true` and a rejection would crash the script under set -e; -# for the deploy line it's required because a silent CheckTx rejection -# leaves SEI_EVM_IO_SEED_BLOCK unset and skips the __SEED__ fixtures. -# The reverter deploy further down only needs protection if the -# minimal deploy's receipt-poll loop didn't already wait (which it -# only skips when DEPLOY_TX is empty — i.e. minimal already failed, -# in which case reverter racing is no worse). -SEQ_BEFORE_ASSOC=$(get_from_seq) -run seid tx evm associate-address --from "$FROM" "${KEYRING_ARGS[@]}" --chain-id sei -b sync -y 2>/dev/null || true -wait_from_seq_advance "$SEQ_BEFORE_ASSOC" -SEQ_BEFORE_SEND=$(get_from_seq) -run seid tx evm send "$RECIPIENT" 1 --from "$FROM" "${KEYRING_ARGS[@]}" --chain-id sei --evm-rpc "$EVM_RPC_URL" -b sync -y -wait_from_seq_advance "$SEQ_BEFORE_SEND" -SEQ_BEFORE_MINIMAL=$(get_from_seq) +# These are EVM transactions, so receipt polling is the reliable +# inclusion signal. Cosmos account sequence waits are not appropriate here. +SEND_OUT=$(run seid tx evm send "$RECIPIENT" 1 --from "$FROM" "${KEYRING_ARGS[@]}" --chain-id sei --evm-rpc "$EVM_RPC_URL" -b sync -y 2>&1) +SEND_TX=$(echo "$SEND_OUT" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) +if [[ -z "$SEND_TX" ]]; then + echo "Failed to extract seed transfer tx hash:" >&2 + printf "%s\n" "$SEND_OUT" >&2 + exit 1 +fi +wait_for_receipt_field "$SEND_TX" blockNumber >/dev/null DEPLOY_OUT=$(run seid tx evm deploy /tmp/minimal_contract.hex --from "$FROM" "${KEYRING_ARGS[@]}" --chain-id sei --evm-rpc "$EVM_RPC_URL" -b sync -y 2>&1) || true DEPLOY_TX=$(echo "$DEPLOY_OUT" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) if [[ -n "$DEPLOY_TX" ]]; then - sleep 2 - for _ in 1 2 3 4 5 6 7 8 9 10; do - RESP=$(docker exec "$CONTAINER" curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$DEPLOY_TX\"]}" "$EVM_RPC_URL" 2>/dev/null) || true - SEED=$(echo "$RESP" | grep -o '"blockNumber":"[^"]*"' | head -1 | cut -d'"' -f4) - [[ -n "$SEED" ]] && break - sleep 1 - done + SEED=$(wait_for_receipt_field "$DEPLOY_TX" blockNumber) || true if [[ -n "$SEED" ]]; then export SEI_EVM_IO_SEED_BLOCK="$SEED" export SEI_EVM_IO_DEPLOY_TX_HASH="$DEPLOY_TX" @@ -102,23 +129,11 @@ if [[ -n "$DEPLOY_TX" ]]; then fi # Deploy reverter contract (reverts with Error("user error")); export SEI_EVM_IO_REVERTER_ADDRESS for .iox __REVERTER__ tag. -# wait_from_seq_advance even though minimal's receipt-poll loop above -# usually does the same job — when the grep fails to extract DEPLOY_TX -# from a successful CheckTx response (CLI format quirk, partial output), -# the receipt poll is skipped entirely and reverter would otherwise -# fire with a stale sequence. docker exec "$CONTAINER" /bin/bash -c "tr -d '[:space:]' < \"$REVERTER_HEX\" > /tmp/reverter_contract.hex" -wait_from_seq_advance "$SEQ_BEFORE_MINIMAL" REVERTER_OUT=$(run seid tx evm deploy /tmp/reverter_contract.hex --from "$FROM" "${KEYRING_ARGS[@]}" --chain-id sei --evm-rpc "$EVM_RPC_URL" -b sync -y 2>&1) || true REVERTER_TX=$(echo "$REVERTER_OUT" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) if [[ -n "$REVERTER_TX" ]]; then - sleep 2 - for _ in 1 2 3 4 5 6 7 8 9 10; do - RRESP=$(docker exec "$CONTAINER" curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$REVERTER_TX\"]}" "$EVM_RPC_URL" 2>/dev/null) || true - REVERTER_ADDR=$(echo "$RRESP" | grep -o '"contractAddress":"[^"]*"' | head -1 | cut -d'"' -f4) - [[ -n "$REVERTER_ADDR" ]] && break - sleep 1 - done + REVERTER_ADDR=$(wait_for_receipt_field "$REVERTER_TX" contractAddress) || true if [[ -n "$REVERTER_ADDR" ]]; then export SEI_EVM_IO_REVERTER_ADDRESS="$REVERTER_ADDR" fi diff --git a/integration_test/evm_module/ws_test/ws_test.go b/integration_test/evm_module/ws_test/ws_test.go index 13e9896508..2db984a66f 100644 --- a/integration_test/evm_module/ws_test/ws_test.go +++ b/integration_test/evm_module/ws_test/ws_test.go @@ -13,11 +13,13 @@ package ws_test import ( + "context" "os" "testing" "time" "github.com/gorilla/websocket" + "github.com/sei-protocol/sei-chain/testutil/evmtest" ) func wsURL() string { @@ -27,6 +29,17 @@ func wsURL() string { return "ws://127.0.0.1:8546" } +func triggerHead(t *testing.T) { + t.Helper() + // Progress-only tx: newHeads needs one real block after subscription, and + // under allow_empty_blocks=false we must create that block explicitly. + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + if _, err := evmtest.SendTinyEvmTx(ctx, evmtest.ConfigFromEnv("SEI_EVM_WS_TX_")); err != nil { + t.Fatalf("trigger head tx: %v", err) + } +} + func TestEthSubscribeNewHeads(t *testing.T) { if os.Getenv("SEI_EVM_WS_RUN_INTEGRATION") != "1" { t.Skip("EVM WS integration tests skipped (set SEI_EVM_WS_RUN_INTEGRATION=1 to run)") @@ -68,8 +81,11 @@ func TestEthSubscribeNewHeads(t *testing.T) { } t.Logf("subscription id: %s", ack.Result) - // Wait for at least one head notification. At Sei's block cadence - // this should arrive within a few seconds; allow generous slack. + // Drive a real block after subscribing so Autobahn does not depend on + // idle block production to emit a new head notification. + triggerHead(t) + + // Wait for the resulting head notification. if err = conn.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil { t.Fatalf("set deadline: %v", err) } diff --git a/integration_test/gov_module/gov_proposal_test.yaml b/integration_test/gov_module/gov_proposal_test.yaml index 23d867bf8d..afb276b165 100644 --- a/integration_test/gov_module/gov_proposal_test.yaml +++ b/integration_test/gov_module/gov_proposal_test.yaml @@ -18,9 +18,8 @@ - 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 + # since quorum is 0.5, we only need 2/4 votes; drive progress until it passes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && wait_for_proposal_status $PROPOSAL_ID PROPOSAL_STATUS_PASSED admin env: PROPOSAL_STATUS # Get the tally params again after proposal is passed - cmd: seid q gov params --output json | jq -r .tally_params.quorum @@ -62,9 +61,8 @@ - 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 + # since quorum is 0.5, we only need 2/4 votes; drive progress until it passes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && wait_for_proposal_status $PROPOSAL_ID PROPOSAL_STATUS_PASSED admin 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 "\"" @@ -107,9 +105,8 @@ - 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 + # since expedited quorum is 0.9, we need 4/4 votes; drive progress until it passes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && wait_for_proposal_status $PROPOSAL_ID PROPOSAL_STATUS_PASSED admin 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 @@ -135,9 +132,8 @@ # only 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 - # since expedited quorum is 0.75, we expect it to be rejected and burn tokens, the since expected proposal will auto convert to normal proposal, we need to wait 35 seconds - - cmd: sleep 35 - - cmd: seid q gov proposal $PROPOSAL_ID --output json | jq -r .status + # the expedited proposal auto-converts to regular; drive progress until it reaches a terminal status + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && wait_for_proposal_status $PROPOSAL_ID PROPOSAL_STATUS_REJECTED admin 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 diff --git a/integration_test/gov_module/staking_proposal_test.yaml b/integration_test/gov_module/staking_proposal_test.yaml index 84ec6235b1..e8701d261c 100644 --- a/integration_test/gov_module/staking_proposal_test.yaml +++ b/integration_test/gov_module/staking_proposal_test.yaml @@ -26,9 +26,8 @@ - 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 + # since expedited quorum is 0.9, we need 4/4 votes; drive progress until it passes + - cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && wait_for_proposal_status $PROPOSAL_ID PROPOSAL_STATUS_PASSED admin env: PROPOSAL_STATUS # Get the params again after proposal is passed - cmd: seid q params subspace staking UnbondingTime --output json | jq -r .value | tr -d "\"" @@ -40,4 +39,4 @@ - type: eval expr: NEW_PARAM == 1814400000000000 - type: eval - expr: NEW_PARAM_FROM_STAKING == "1814400s" \ No newline at end of file + expr: NEW_PARAM_FROM_STAKING == "1814400s" diff --git a/integration_test/rpc_tests/utils/gasPriceUtils.ts b/integration_test/rpc_tests/utils/gasPriceUtils.ts index 8ae0039c93..f7beeeab39 100644 --- a/integration_test/rpc_tests/utils/gasPriceUtils.ts +++ b/integration_test/rpc_tests/utils/gasPriceUtils.ts @@ -39,23 +39,23 @@ export interface PriorityFeeSample { export async function maxPriorityFeePerGasAtStableBlock( provider: ethers.JsonRpcProvider, ): Promise { - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 50; i++) { const b1 = await provider.getBlockNumber(); const tip = await maxPriorityFeePerGas(provider); - const info = await blockGasInfo(provider, 'latest'); - const evmGasUsed = await evmGasUsedForBlock(provider, info.number); const b2 = await provider.getBlockNumber(); - if (b1 === b2 && info.number === b1) { - return { - tip, - block: info.number, - gasUsed: info.gasUsed, - evmGasUsed, - gasLimit: info.gasLimit, - ratio: Number(info.gasUsed) / Number(info.gasLimit), - evmRatio: Number(evmGasUsed) / Number(info.gasLimit), - }; - } + if (b1 !== b2) continue; + + const info = await blockGasInfo(provider, b1); + const evmGasUsed = await evmGasUsedForBlock(provider, b1); + return { + tip, + block: info.number, + gasUsed: info.gasUsed, + evmGasUsed, + gasLimit: info.gasLimit, + ratio: Number(info.gasUsed) / Number(info.gasLimit), + evmRatio: Number(evmGasUsed) / Number(info.gasLimit), + }; } throw new Error('maxPriorityFeePerGasAtStableBlock: block kept advancing across the sample'); } diff --git a/integration_test/seidb/flatkv_evm_test.yaml b/integration_test/seidb/flatkv_evm_test.yaml index 4cf716837f..cf5e970a2c 100644 --- a/integration_test/seidb/flatkv_evm_test.yaml +++ b/integration_test/seidb/flatkv_evm_test.yaml @@ -86,7 +86,7 @@ - name: Test non-EVM module remains queryable inputs: - - cmd: printf "12345678\n" | seid keys list --output json | jq -r ".[0].address" + - cmd: printf "12345678\n" | seid keys show admin -a env: SEI_ADDR - cmd: seid q bank balances $SEI_ADDR --output json | jq -r ".balances | length" env: BANK_BALANCE_COUNT diff --git a/integration_test/startup/startup_test.yaml b/integration_test/startup/startup_test.yaml index bb9ff5fc61..c6fad56cba 100644 --- a/integration_test/startup/startup_test.yaml +++ b/integration_test/startup/startup_test.yaml @@ -7,11 +7,11 @@ - type: eval expr: RESULT == 4 -- name: Test block height should be greater than 0 +- name: Test node status should report the configured network inputs: - # Get the current block height - - cmd: seid status |jq -M -r .SyncInfo.latest_block_height + # Confirm status RPC is ready without relying on block production. + - cmd: seid status |jq -M -r .NodeInfo.network env: RESULT verifiers: - type: eval - expr: RESULT > 0 + expr: RESULT == "sei" diff --git a/integration_test/upgrade_module/scripts/proposal_wait_for_pass.sh b/integration_test/upgrade_module/scripts/proposal_wait_for_pass.sh index 84135cccb5..3b2d5ee96b 100755 --- a/integration_test/upgrade_module/scripts/proposal_wait_for_pass.sh +++ b/integration_test/upgrade_module/scripts/proposal_wait_for_pass.sh @@ -1,22 +1,13 @@ #!/bin/bash PROPOSAL_ID=$1 -TIMEOUT=300 # total wait time in seconds -INTERVAL=1 # time between checks in seconds -TRIES=$((TIMEOUT / INTERVAL)) # number of tries - -# Loop until the proposal status is PROPOSAL_STATUS_PASSED or we timeout -for ((i=1; i<=TRIES; i++)); do - STATUS=$(seid query gov proposal $PROPOSAL_ID --output json | jq -r ".status") - - if [ "$STATUS" == "PROPOSAL_STATUS_PASSED" ]; then - echo "Proposal $PROPOSAL_ID has passed!" - exit 0 - else - echo "Waiting for proposal $PROPOSAL_ID to pass... ($i/$TRIES)" - sleep $INTERVAL - fi -done - -echo "Timeout reached. Exiting." -exit 1 +TIMEOUT=300 +seidbin=seid +chainid=sei +source integration_test/utils/_tx_helpers.sh + +if wait_for_proposal_status "$PROPOSAL_ID" "PROPOSAL_STATUS_PASSED" "admin" "$TIMEOUT" >/dev/null; then + echo "Proposal $PROPOSAL_ID has passed!" +else + exit 1 +fi diff --git a/integration_test/utils/_tx_helpers.sh b/integration_test/utils/_tx_helpers.sh index 33cdc5c027..3ba9a2330e 100644 --- a/integration_test/utils/_tx_helpers.sh +++ b/integration_test/utils/_tx_helpers.sh @@ -20,6 +20,10 @@ _get_account_sequence() { $seidbin q account "$1" -o json 2>/dev/null | jq -r '.sequence // 0' } +_get_key_address() { + printf "12345678\n" | $seidbin keys show "$1" -a 2>/dev/null +} + # Cosmos account sequence for an address at a historical height. Echoes # the sequence number (0 if the account didn't exist yet at ), # or empty on query failure — distinct from 0 so the caller can retry @@ -48,7 +52,9 @@ _wait_until() { # Submit `bank send` via -b sync and echo the exact block height at # which the tx committed. Useful when callers need the inclusion # height for state-at-height queries (e.g., historical balance lookups -# at height-1 vs height to validate per-block granularity). +# at height-1 vs height to validate per-block granularity). Some +# callers intentionally self-send a dust amount purely to force the +# chain to produce a real block under allow_empty_blocks=false. # Implementation: after the sender's sequence advances, walk back from # the observed height querying historical sequence — the largest H # where sequence(H) is still pre-tx is one below the inclusion height. @@ -57,7 +63,7 @@ bank_send_and_get_height() { local from_key="$1" local to_addr="$2" local amount_denom="$3" - local from_addr; from_addr=$(printf "12345678\n" | $seidbin keys show "$from_key" -a 2>/dev/null) + local from_addr; from_addr=$(_get_key_address "$from_key") local seq_before; seq_before=$(_get_account_sequence "$from_addr") local resp; resp=$(printf "12345678\n" | $seidbin tx bank send "$from_key" "$to_addr" "$amount_denom" \ -y --chain-id="$chainid" --gas=5000000 --fees=1000000usei \ @@ -110,6 +116,52 @@ wait_until_height_exceeds() { "[ \$($seidbin status | jq -r .SyncInfo.latest_block_height) -gt $min_height ]" } +get_proposal_status() { + $seidbin q gov proposal "$1" --output json 2>/dev/null | jq -r '.status // ""' +} + +wait_for_proposal_status() { + local proposal_id="$1" + local target_status="$2" + local from_key="${3:-admin}" + local timeout_secs="${4:-120}" + local from_addr; from_addr=$(_get_key_address "$from_key") + local deadline=$(($(date +%s) + timeout_secs)) + local status="" + while [ "$(date +%s)" -lt "$deadline" ]; do + local raw; raw=$($seidbin q gov proposal "$proposal_id" --output json 2>/dev/null || true) + status=$(echo "$raw" | jq -r '.status // ""' 2>/dev/null) + if [ "$status" = "$target_status" ]; then + echo "$status" + return 0 + fi + if [ "$status" = "PROPOSAL_STATUS_REJECTED" ] || [ "$status" = "PROPOSAL_STATUS_FAILED" ]; then + echo "proposal $proposal_id reached terminal status $status while waiting for $target_status" >&2 + return 1 + fi + local voting_end + voting_end=$(echo "$raw" | jq -r '.voting_end_time // ""' 2>/dev/null) + local voting_end_epoch="" + if [ -n "$voting_end" ]; then + voting_end_epoch=$(date -d "$voting_end" +%s 2>/dev/null || true) + fi + if [ -z "$voting_end_epoch" ]; then + echo "proposal $proposal_id missing valid voting_end_time while waiting for $target_status" >&2 + return 1 + fi + if [ "$(date +%s)" -ge $((voting_end_epoch + 1)) ]; then + # Progress-only self-send: once the currently observed voting end + # time has passed, force one committed block so tallying can + # advance. Expedited proposals can convert to regular and extend + # voting_end_time, so re-read proposal state after every kick. + bank_send_and_wait "$from_key" "$from_addr" "1usei" >/dev/null || return 1 + fi + sleep 1 + done + echo "timed out waiting for proposal $proposal_id to reach $target_status (last status=${status:-unknown})" >&2 + return 1 +} + # Submit `tx ` via -b sync from , wait for # that sender's account sequence to advance, and echo the CheckTx code # (0 on success). On CheckTx rejection the rejection log is written to @@ -119,7 +171,7 @@ wait_until_height_exceeds() { # Usage: code=$(submit_tx_and_wait ) submit_tx_and_wait() { local from_key="$1"; shift - local from_addr; from_addr=$(printf "12345678\n" | $seidbin keys show "$from_key" -a 2>/dev/null) + local from_addr; from_addr=$(_get_key_address "$from_key") local seq_before; seq_before=$(_get_account_sequence "$from_addr") local resp; resp=$(printf "12345678\n" | $seidbin tx "$@" --from "$from_key" \ -y --chain-id="$chainid" --broadcast-mode=sync --output=json) @@ -201,7 +253,7 @@ bank_send_and_wait() { local from_key="$1" local to_addr="$2" local amount_denom="$3" - local from_addr; from_addr=$(printf "12345678\n" | $seidbin keys show "$from_key" -a 2>/dev/null) + local from_addr; from_addr=$(_get_key_address "$from_key") local seq_before; seq_before=$(_get_account_sequence "$from_addr") local resp; resp=$(printf "12345678\n" | $seidbin tx bank send "$from_key" "$to_addr" "$amount_denom" \ -y --chain-id="$chainid" --gas=5000000 --fees=1000000usei \ diff --git a/sei-cosmos/baseapp/abci.go b/sei-cosmos/baseapp/abci.go index e6478fffbf..1ab8896750 100644 --- a/sei-cosmos/baseapp/abci.go +++ b/sei-cosmos/baseapp/abci.go @@ -30,9 +30,14 @@ import ( grpcstatus "google.golang.org/grpc/status" ) +func (app *BaseApp) InitLastHeader(lastHeader *tmproto.Header) { + app.setCheckState(*lastHeader) + app.signalInitialized() +} + // InitChain implements the ABCI interface. It runs the initialization logic // directly on the CommitMultiStore. -func (app *BaseApp) InitChain(ctx context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { +func (app *BaseApp) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { // On a new chain, we consider the init chain block height as 0, even though // req.InitialHeight is 1 by default. initHeader := tmproto.Header{ChainID: req.ChainId, Time: req.Time} @@ -65,12 +70,17 @@ func (app *BaseApp) InitChain(ctx context.Context, req *abci.RequestInitChain) ( app.SetDeliverStateToCommit() + defer app.signalInitialized() if app.initChainer == nil { return nil, nil } - resp := app.initChainer(app.deliverState.ctx, *req) - app.initChainer(app.processProposalState.ctx, *req) + resp := app.initChainer(app.deliverState.ctx.WithIsGenesis(true), *req) + app.initChainer(app.processProposalState.ctx.WithIsGenesis(true), *req) + // Genesis initialization needs deliver semantics even when populating the + // check state branch. Running it in CheckTx mode enables mempool-only fee + // checks and can reject valid genesis txs. + app.initChainer(app.checkState.ctx.WithIsGenesis(true).WithIsCheckTx(false), *req) // In the case of a new chain, AppHash will be the hash of an empty string. // During an upgrade, it'll be the hash of the last committed block. @@ -91,7 +101,7 @@ func (app *BaseApp) InitChain(ctx context.Context, req *abci.RequestInitChain) ( } // Info implements the ABCI interface. -func (app *BaseApp) Info(ctx context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { +func (app *BaseApp) Info() *abci.ResponseInfo { lastCommitID := app.cms.LastCommitID() return &abci.ResponseInfo{ @@ -101,7 +111,7 @@ func (app *BaseApp) Info(ctx context.Context, req *abci.RequestInfo) (*abci.Resp LastBlockHeight: lastCommitID.Version, LastBlockAppHash: lastCommitID.Hash, MinimumGasPrices: app.minGasPrices.String(), - }, nil + } } func (app *BaseApp) MidBlock(ctx sdk.Context, height int64) (events []abci.Event) { @@ -315,6 +325,7 @@ func (app *BaseApp) Commit(ctx context.Context) (res *abci.ResponseCommit, err e // empty/reset the deliver state app.resetStatesExceptCheckState() + defer app.signalInitialized() var halt bool @@ -689,22 +700,28 @@ func (app *BaseApp) CreateQueryContext(height int64, prove bool) (sdk.Context, e ) } + checkStateCtx := app.checkState.Context() var cacheMS types.CacheMultiStore - if height < app.migrationHeight && app.qms != nil { - cacheMS, err = app.qms.CacheMultiStoreWithVersion(height) + if lastBlockHeight == 0 && height == 0 { + // Height 0 means "latest". Before the first Commit, the latest readable + // state lives only on checkState (e.g. InitChain writes). Querying the + // committed multistore at version 0 would incorrectly hide that state. + cacheMS = app.checkState.MultiStore().CacheMultiStore() } else { - cacheMS, err = app.cms.CacheMultiStoreWithVersion(height) - } + if height < app.migrationHeight && app.qms != nil { + cacheMS, err = app.qms.CacheMultiStoreWithVersion(height) + } else { + cacheMS, err = app.cms.CacheMultiStoreWithVersion(height) + } - if err != nil { - return sdk.Context{}, - sdkerrors.Wrapf( - sdkerrors.ErrInvalidRequest, - "failed to load state at height %d; %s (latest height: %d)", height, err, lastBlockHeight, - ) + if err != nil { + return sdk.Context{}, + sdkerrors.Wrapf( + sdkerrors.ErrInvalidRequest, + "failed to load state at height %d; %s (latest height: %d)", height, err, lastBlockHeight, + ) + } } - - checkStateCtx := app.checkState.Context() // branch the commit-multistore for safety ctx := sdk.NewContext( cacheMS, checkStateCtx.BlockHeader(), true, diff --git a/sei-cosmos/baseapp/abci_test.go b/sei-cosmos/baseapp/abci_test.go index 99b2622962..a705dbe8ca 100644 --- a/sei-cosmos/baseapp/abci_test.go +++ b/sei-cosmos/baseapp/abci_test.go @@ -105,7 +105,7 @@ func TestGetBlockRentionHeight(t *testing.T) { tc := tc tc.bapp.SetParamStore(¶mStore{db: dbm.NewMemDB()}) - tc.bapp.InitChain(context.Background(), &abci.RequestInitChain{ + tc.bapp.InitChain(&abci.RequestInitChain{ ConsensusParams: &tmproto.ConsensusParams{ Evidence: &tmproto.EvidenceParams{ MaxAgeNumBlocks: tc.maxAgeBlocks, @@ -164,6 +164,32 @@ func TestBaseAppCreateQueryContext(t *testing.T) { } } +func TestCreateQueryContextUsesCheckStateBeforeFirstCommit(t *testing.T) { + t.Parallel() + + capKey := sdk.NewKVStoreKey("genesis") + key := []byte("hello") + value := []byte("world") + initChainerOpt := func(bapp *BaseApp) { + bapp.SetInitChainer(func(ctx sdk.Context, _ abci.RequestInitChain) abci.ResponseInitChain { + ctx.KVStore(capKey).Set(key, value) + return abci.ResponseInitChain{} + }) + } + app := newBaseApp(t.Name(), initChainerOpt) + app.MountStores(capKey1, capKey2, capKey) + app.SetParamStore(¶mStore{db: dbm.NewMemDB()}) + require.NoError(t, app.LoadLatestVersion()) + + _, err := app.InitChain(&abci.RequestInitChain{ChainId: "sei"}) + require.NoError(t, err) + require.Equal(t, int64(0), app.LastBlockHeight()) + + ctx, err := app.CreateQueryContext(0, false) + require.NoError(t, err) + require.Equal(t, value, ctx.KVStore(capKey).Get(key)) +} + type paramStore struct { db *dbm.MemDB } @@ -256,7 +282,7 @@ func TestProcessProposalCleanContextNotAffectedByHandler(t *testing.T) { err := app.LoadLatestVersion() require.NoError(t, err) - app.InitChain(context.Background(), &abci.RequestInitChain{ + app.InitChain(&abci.RequestInitChain{ AppStateBytes: []byte("{}"), ChainId: "test-chain", }) diff --git a/sei-cosmos/baseapp/baseapp.go b/sei-cosmos/baseapp/baseapp.go index 69401993b9..43d8e41cf1 100644 --- a/sei-cosmos/baseapp/baseapp.go +++ b/sei-cosmos/baseapp/baseapp.go @@ -121,6 +121,8 @@ type BaseApp struct { processProposalState *state processProposalCleanCtx sdk.Context // snapshot before optimistic processing stateToCommit *state + initializedCh chan struct{} + initializedOnce sync.Once // nextResultHash is the result hash (merkle root over the block's deterministic tx results) // computed in FinalizeBlock and handed to the commit store in Commit, mirroring nextBlockHash. @@ -289,6 +291,7 @@ func NewBaseApp( TracingInfo: tracing.NewTracingInfo(tr, tracingEnabled), commitLock: &sync.Mutex{}, checkTxStateLock: &sync.RWMutex{}, + initializedCh: make(chan struct{}), deliverTxHooks: []DeliverTxHook{}, } @@ -340,6 +343,16 @@ func (app *BaseApp) OccEnabled() bool { return app.occEnabled } +func (app *BaseApp) Initialized() <-chan struct{} { + return app.initializedCh +} + +func (app *BaseApp) signalInitialized() { + app.initializedOnce.Do(func() { + close(app.initializedCh) + }) +} + // Version returns the application's version string. func (app *BaseApp) Version() string { return app.version diff --git a/sei-cosmos/baseapp/deliver_tx_batch_test.go b/sei-cosmos/baseapp/deliver_tx_batch_test.go index 060f28d931..c0d7318c15 100644 --- a/sei-cosmos/baseapp/deliver_tx_batch_test.go +++ b/sei-cosmos/baseapp/deliver_tx_batch_test.go @@ -99,7 +99,7 @@ func TestDeliverTxBatch(t *testing.T) { } app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) // Create same codec used in txDecoder codec := codec.NewLegacyAmino() @@ -158,7 +158,7 @@ func TestDeliverTxBatchEmpty(t *testing.T) { } app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) // Create same codec used in txDecoder codec := codec.NewLegacyAmino() diff --git a/sei-cosmos/baseapp/deliver_tx_test.go b/sei-cosmos/baseapp/deliver_tx_test.go index 9e21813760..7e8fc811a3 100644 --- a/sei-cosmos/baseapp/deliver_tx_test.go +++ b/sei-cosmos/baseapp/deliver_tx_test.go @@ -198,7 +198,7 @@ func TestWithRouter(t *testing.T) { } app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) // Create same codec used in txDecoder codec := codec.NewLegacyAmino() @@ -242,7 +242,7 @@ func TestBaseApp_EndBlock(t *testing.T) { codec := codec.NewLegacyAmino() app := NewBaseApp(name, db, testTxDecoder(codec), nil, &testutil.TestAppOpts{}) app.SetParamStore(¶mStore{db: dbm.NewMemDB()}) - app.InitChain(context.Background(), &abci.RequestInitChain{ + app.InitChain(&abci.RequestInitChain{ ConsensusParams: cp, }) @@ -284,7 +284,7 @@ func TestQuery(t *testing.T) { app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) // NOTE: "/store/bank" tells us KVStore // and the final "/key" says to use the data as the @@ -333,7 +333,7 @@ func TestGRPCQuery(t *testing.T) { app := setupBaseApp(t, grpcQueryOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) header := tmproto.Header{Height: app.LastBlockHeight() + 1} app.setDeliverState(header) app.SetDeliverStateToCommit() @@ -457,7 +457,7 @@ func TestSimulateTx(t *testing.T) { app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) // Create same codec used in txDecoder cdc := codec.NewLegacyAmino() @@ -508,6 +508,72 @@ func TestSimulateTx(t *testing.T) { } } +func TestInitChainGenesisFlagDoesNotLeakToCheckOrSimulateBeforeFirstCommit(t *testing.T) { + type anteCall struct { + simulate bool + isCheckTx bool + isSimulation bool + isGenesis bool + } + + var anteCalls []anteCall + anteOpt := func(bapp *BaseApp) { + bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + anteCalls = append(anteCalls, anteCall{ + simulate: simulate, + isCheckTx: ctx.IsCheckTx(), + isSimulation: ctx.IsSimulation(), + isGenesis: ctx.IsGenesis(), + }) + return ctx, nil + }) + } + + routerOpt := func(bapp *BaseApp) { + r := sdk.NewRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + return &sdk.Result{}, nil + }) + bapp.Router().AddRoute(r) + } + + var initGenesisFlags []bool + initChainerOpt := func(bapp *BaseApp) { + bapp.SetInitChainer(func(ctx sdk.Context, _ abci.RequestInitChain) abci.ResponseInitChain { + initGenesisFlags = append(initGenesisFlags, ctx.IsGenesis()) + return abci.ResponseInitChain{} + }) + } + + app := newBaseApp(t.Name(), anteOpt, routerOpt, initChainerOpt) + app.MountStores(capKey1, capKey2) + app.SetParamStore(¶mStore{db: dbm.NewMemDB()}) + require.NoError(t, app.LoadLatestVersion()) + + _, err := app.InitChain(&abci.RequestInitChain{}) + require.NoError(t, err) + require.Equal(t, []bool{true, true, true}, initGenesisFlags) + require.Equal(t, int64(0), app.LastBlockHeight()) + + tx := newTxCounter(0, 0) + _, _, err = app.Check(aminoTxEncoder(), tx) + require.NoError(t, err) + + txBytes, err := aminoTxEncoder()(tx) + require.NoError(t, err) + _, _, err = app.Simulate(txBytes) + require.NoError(t, err) + + require.Len(t, anteCalls, 2) + require.False(t, anteCalls[0].simulate) + require.True(t, anteCalls[0].isCheckTx) + require.False(t, anteCalls[0].isSimulation) + require.False(t, anteCalls[0].isGenesis) + require.True(t, anteCalls[1].simulate) + require.True(t, anteCalls[1].isCheckTx) + require.True(t, anteCalls[1].isSimulation) + require.False(t, anteCalls[1].isGenesis) +} + func TestRunInvalidTransaction(t *testing.T) { anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { @@ -749,7 +815,7 @@ func TestTxGasLimits(t *testing.T) { // } // app := setupBaseApp(t, anteOpt, routerOpt) -// app.InitChain(context.Background(), &abci.RequestInitChain{ +// app.InitChain(&abci.RequestInitChain{ // ConsensusParams: &tmproto.ConsensusParams{ // Block: &tmproto.BlockParams{ // MaxGas: 100, @@ -881,7 +947,7 @@ func TestBaseAppAnteHandler(t *testing.T) { cdc := codec.NewLegacyAmino() app := setupBaseApp(t, anteOpt, routerOpt, preCommitHandlerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) registerTestCodec(cdc) header := tmproto.Header{Height: app.LastBlockHeight() + 1} @@ -969,7 +1035,7 @@ func TestPrecommitHandlerPanic(t *testing.T) { cdc := codec.NewLegacyAmino() app := setupBaseApp(t, anteOpt, routerOpt, preCommitHandlerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) registerTestCodec(cdc) header := tmproto.Header{Height: app.LastBlockHeight() + 1} @@ -1078,7 +1144,7 @@ func TestGasConsumptionBadTx(t *testing.T) { registerTestCodec(cdc) app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{ + app.InitChain(&abci.RequestInitChain{ ConsensusParams: &tmproto.ConsensusParams{ Block: &tmproto.BlockParams{ MaxGas: 9, @@ -1086,7 +1152,7 @@ func TestGasConsumptionBadTx(t *testing.T) { }, }) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) header := tmproto.Header{Height: app.LastBlockHeight() + 1} app.setDeliverState(header) @@ -1128,7 +1194,7 @@ func TestInitChainer(t *testing.T) { } // initChainer is nil - nothing happens - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) res, _ := app.Query(context.Background(), &query) require.Equal(t, 0, len(res.Value)) @@ -1140,7 +1206,7 @@ func TestInitChainer(t *testing.T) { require.Nil(t, err) require.Equal(t, int64(0), app.LastBlockHeight()) - initChainRes, _ := app.InitChain(context.Background(), &abci.RequestInitChain{AppStateBytes: []byte("{}"), ChainId: "test-chain-id"}) // must have valid JSON genesis file, even if empty + initChainRes, _ := app.InitChain(&abci.RequestInitChain{AppStateBytes: []byte("{}"), ChainId: "test-chain-id"}) // must have valid JSON genesis file, even if empty // The AppHash returned by a new chain is the sha256 hash of "". // $ echo -n '' | sha256sum @@ -1157,6 +1223,7 @@ func TestInitChainer(t *testing.T) { chainID = app.checkState.ctx.ChainID() require.Equal(t, "test-chain-id", chainID, "ChainID in checkState not set correctly in InitChain") + require.Equal(t, int64(0), app.checkState.ctx.BlockHeight(), "checkState height should remain at the latest committed height after InitChain") app.Commit(context.Background()) res, _ = app.Query(context.Background(), &query) @@ -1193,10 +1260,9 @@ func TestInitChain_WithInitialHeight(t *testing.T) { cc := codec.NewLegacyAmino() app := NewBaseApp(name, db, testTxDecoder(cc), nil, &testutil.TestAppOpts{}) - app.InitChain( - context.Background(), &abci.RequestInitChain{ - InitialHeight: 3, - }, + app.InitChain(&abci.RequestInitChain{ + InitialHeight: 3, + }, ) app.Commit(context.Background()) @@ -1444,7 +1510,7 @@ func TestDeliverTx(t *testing.T) { } app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) // Create same codec used in txDecoder codec := codec.NewLegacyAmino() @@ -1507,7 +1573,7 @@ func TestDeliverTxHooks(t *testing.T) { } app := setupBaseApp(t, anteOpt, routerOpt) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) // Create same codec used in txDecoder codec := codec.NewLegacyAmino() @@ -1576,8 +1642,7 @@ func TestInfo(t *testing.T) { app := newBaseApp(t.Name()) // ----- test an empty response ------- - reqInfo := abci.RequestInfo{} - res, _ := app.Info(context.Background(), &reqInfo) + res := app.Info() // should be empty assert.Equal(t, "", res.Version) @@ -1711,7 +1776,7 @@ func setupBaseAppWithSnapshots(t *testing.T, blocks uint, blockTxs int, options SetPruning(sdk.PruningOptions{KeepEvery: 1}), routerOpt)...) - app.InitChain(context.Background(), &abci.RequestInitChain{}) + app.InitChain(&abci.RequestInitChain{}) r := rand.New(rand.NewSource(3920758213583)) keyCounter := 0 diff --git a/sei-cosmos/client/broadcast_test.go b/sei-cosmos/client/broadcast_test.go index 917c5b882d..c0bc028c7d 100644 --- a/sei-cosmos/client/broadcast_test.go +++ b/sei-cosmos/client/broadcast_test.go @@ -6,7 +6,6 @@ import ( "fmt" "testing" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" ctypes "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" "github.com/stretchr/testify/require" @@ -16,7 +15,7 @@ import ( ) type MockClient struct { - mock.Client + Client err error } diff --git a/sei-cosmos/server/mock/app_test.go b/sei-cosmos/server/mock/app_test.go index b0f68f9f98..e737b21610 100644 --- a/sei-cosmos/server/mock/app_test.go +++ b/sei-cosmos/server/mock/app_test.go @@ -23,7 +23,7 @@ func TestInitApp(t *testing.T) { req := abci.RequestInitChain{ AppStateBytes: appState, } - app.InitChain(context.Background(), &req) + app.InitChain(&req) app.Commit(context.Background()) // make sure we can query these values @@ -54,7 +54,7 @@ func TestDeliverTx(t *testing.T) { req := abci.RequestInitChain{ AppStateBytes: appState, } - app.InitChain(goCtx, &req) + app.InitChain(&req) header := tmproto.Header{Height: 1} app.FinalizeBlock(goCtx, &abci.RequestFinalizeBlock{ Hash: []byte("apphash"), diff --git a/sei-cosmos/server/rollback_test.go b/sei-cosmos/server/rollback_test.go index cecd249253..1f5f4bbf8d 100644 --- a/sei-cosmos/server/rollback_test.go +++ b/sei-cosmos/server/rollback_test.go @@ -60,14 +60,16 @@ func (m *mockApplication) Close() error { } // Implement other required methods with no-ops -func (m *mockApplication) Info(ctx context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { - return &abci.ResponseInfo{}, nil +func (m *mockApplication) Info() *abci.ResponseInfo { + return &abci.ResponseInfo{} } -func (m *mockApplication) InitChain(ctx context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { +func (m *mockApplication) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { return &abci.ResponseInitChain{}, nil } +func (m *mockApplication) InitLastHeader(*tmproto.Header) {} + func (m *mockApplication) Query(ctx context.Context, req *abci.RequestQuery) (*abci.ResponseQuery, error) { return &abci.ResponseQuery{}, nil } diff --git a/sei-cosmos/server/start_test.go b/sei-cosmos/server/start_test.go new file mode 100644 index 0000000000..9fe943805f --- /dev/null +++ b/sei-cosmos/server/start_test.go @@ -0,0 +1,47 @@ +package server + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-cosmos/telemetry" +) + +func TestInjectTelemetryChainID(t *testing.T) { + t.Run("appends chain id when missing", func(t *testing.T) { + cfg := telemetry.Config{ + GlobalLabels: [][]string{{"foo", "bar"}}, + } + + got := injectTelemetryChainID(cfg, "sei-test-1") + + require.Equal(t, [][]string{ + {"foo", "bar"}, + {"chain_id", "sei-test-1"}, + }, got.GlobalLabels) + }) + + t.Run("preserves existing chain id label", func(t *testing.T) { + cfg := telemetry.Config{ + GlobalLabels: [][]string{ + {"foo", "bar"}, + {"chain_id", "existing-chain"}, + }, + } + + got := injectTelemetryChainID(cfg, "sei-test-1") + + require.Equal(t, cfg.GlobalLabels, got.GlobalLabels) + }) + + t.Run("ignores empty chain id", func(t *testing.T) { + cfg := telemetry.Config{ + GlobalLabels: [][]string{{"foo", "bar"}}, + } + + got := injectTelemetryChainID(cfg, "") + + require.Equal(t, cfg.GlobalLabels, got.GlobalLabels) + }) +} diff --git a/sei-cosmos/testutil/cli/tm_mocks.go b/sei-cosmos/testutil/cli/tm_mocks.go index e3fcf2de43..d428ee20fc 100644 --- a/sei-cosmos/testutil/cli/tm_mocks.go +++ b/sei-cosmos/testutil/cli/tm_mocks.go @@ -6,7 +6,6 @@ import ( abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" tmbytes "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" rpcclient "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client" - rpcclientmock "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" @@ -16,14 +15,14 @@ import ( var _ client.TendermintRPC = (*MockTendermintRPC)(nil) type MockTendermintRPC struct { - rpcclientmock.Client + rpcclient.Client responseQuery abci.ResponseQuery } // NewMockTendermintRPC returns a mock TendermintRPC implementation. // It is used for CLI testing. -func NewMockTendermintRPC(respQuery abci.ResponseQuery, client rpcclientmock.Client) MockTendermintRPC { +func NewMockTendermintRPC(respQuery abci.ResponseQuery, client rpcclient.Client) MockTendermintRPC { return MockTendermintRPC{ Client: client, responseQuery: respQuery, diff --git a/sei-cosmos/types/context.go b/sei-cosmos/types/context.go index a8f8740a53..f21a87b4e8 100644 --- a/sei-cosmos/types/context.go +++ b/sei-cosmos/types/context.go @@ -41,6 +41,7 @@ type Context struct { blockGasMeter GasMeter checkTx bool recheckTx bool // if recheckTx == true, then checkTx must also be true + isGenesis bool minGasPrice DecCoins consParams *tmproto.ConsensusParams eventManager *EventManager @@ -132,6 +133,10 @@ func (c Context) IsReCheckTx() bool { return c.recheckTx } +func (c Context) IsGenesis() bool { + return c.isGenesis +} + func (c Context) IsOCCEnabled() bool { return c.occEnabled } @@ -379,6 +384,11 @@ func (c Context) WithIsReCheckTx(isRecheckTx bool) Context { return c } +func (c Context) WithIsGenesis(isGenesis bool) Context { + c.isGenesis = isGenesis + return c +} + // WithMinGasPrices returns a Context with an updated minimum gas price value func (c Context) WithMinGasPrices(gasPrices DecCoins) Context { c.minGasPrice = gasPrices diff --git a/sei-cosmos/types/context_test.go b/sei-cosmos/types/context_test.go index 1cbeaa5e89..d91f708326 100644 --- a/sei-cosmos/types/context_test.go +++ b/sei-cosmos/types/context_test.go @@ -94,6 +94,8 @@ func (s *contextTestSuite) TestContextWithCustom() { s.Require().Equal(minGasPrices, ctx.MinGasPrices()) s.Require().Equal(headerHash, ctx.HeaderHash().Bytes()) s.Require().False(ctx.WithIsCheckTx(false).IsCheckTx()) + s.Require().False(ctx.IsGenesis()) + s.Require().True(ctx.WithIsGenesis(true).IsGenesis()) // test IsReCheckTx s.Require().False(ctx.IsReCheckTx()) diff --git a/sei-cosmos/x/auth/ante/ante_test.go b/sei-cosmos/x/auth/ante/ante_test.go index 1fd8c973de..38dcaac7d3 100644 --- a/sei-cosmos/x/auth/ante/ante_test.go +++ b/sei-cosmos/x/auth/ante/ante_test.go @@ -243,9 +243,9 @@ func (suite *AnteTestSuite) TestAnteHandlerAccountNumbers() { } // Test logic around account number checking with many signers when BlockHeight is 0. -func (suite *AnteTestSuite) TestAnteHandlerAccountNumbersAtBlockHeightZero() { +func (suite *AnteTestSuite) TestAnteHandlerAccountNumbersAtGenesis() { suite.SetupTest(false) // setup - suite.ctx = suite.ctx.WithBlockHeight(0) + suite.ctx = suite.ctx.WithBlockHeight(0).WithIsGenesis(true) // Same data for every test cases accounts := suite.CreateTestAccounts(2) diff --git a/sei-cosmos/x/auth/ante/setup.go b/sei-cosmos/x/auth/ante/setup.go index ddd59c9c43..05547fe807 100644 --- a/sei-cosmos/x/auth/ante/setup.go +++ b/sei-cosmos/x/auth/ante/setup.go @@ -83,10 +83,10 @@ func (sud SetUpContextDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate // SetGasMeter returns a new context with a gas meter set from a given context. func SetGasMeter(simulate bool, ctx sdk.Context, gasLimit uint64, _ sdk.Tx) sdk.Context { - // In various cases such as simulation and during the genesis block, we do not + // In various cases such as simulation and genesis delivery, we do not // meter any gas utilization. - if simulate || ctx.BlockHeight() == 0 { + if simulate || ctx.IsGenesis() { return ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) } diff --git a/sei-cosmos/x/auth/ante/sigverify.go b/sei-cosmos/x/auth/ante/sigverify.go index ed619a6fb7..f9c7ab5730 100644 --- a/sei-cosmos/x/auth/ante/sigverify.go +++ b/sei-cosmos/x/auth/ante/sigverify.go @@ -277,10 +277,10 @@ func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul } // retrieve signer data - genesis := ctx.BlockHeight() == 0 + // Genesis gentxs are delivered under InitChain before normal account-number semantics apply. chainID := ctx.ChainID() var accNum uint64 - if !genesis { + if !ctx.IsGenesis() { accNum = acc.GetAccountNumber() } signerData := authsigning.SignerData{ diff --git a/sei-cosmos/x/auth/module_test.go b/sei-cosmos/x/auth/module_test.go index 8b87dae880..f2cf552474 100644 --- a/sei-cosmos/x/auth/module_test.go +++ b/sei-cosmos/x/auth/module_test.go @@ -1,7 +1,6 @@ package auth_test import ( - "context" "testing" "github.com/sei-protocol/sei-chain/app" @@ -16,11 +15,10 @@ func TestItCreatesModuleAccountOnInitBlock(t *testing.T) { app := app.Setup(t, false, false, false) ctx := app.BaseApp.NewContext(false, tmproto.Header{}) - app.InitChain( - context.Background(), &abcitypes.RequestInitChain{ - AppStateBytes: []byte("{}"), - ChainId: "test-chain-id", - }, + app.InitChain(&abcitypes.RequestInitChain{ + AppStateBytes: []byte("{}"), + ChainId: "test-chain-id", + }, ) acc := app.AccountKeeper.GetAccount(ctx, types.NewModuleAddress(types.FeeCollectorName)) diff --git a/sei-cosmos/x/distribution/module_test.go b/sei-cosmos/x/distribution/module_test.go index 1d03215890..d4bd58e4d8 100644 --- a/sei-cosmos/x/distribution/module_test.go +++ b/sei-cosmos/x/distribution/module_test.go @@ -1,7 +1,6 @@ package distribution_test import ( - "context" "testing" abcitypes "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -17,11 +16,10 @@ func TestItCreatesModuleAccountOnInitBlock(t *testing.T) { app := seiapp.Setup(t, false, false, false) ctx := app.BaseApp.NewContext(false, tmproto.Header{}) - app.InitChain( - context.Background(), &abcitypes.RequestInitChain{ - AppStateBytes: []byte("{}"), - ChainId: "test-chain-id", - }, + app.InitChain(&abcitypes.RequestInitChain{ + AppStateBytes: []byte("{}"), + ChainId: "test-chain-id", + }, ) acc := app.AccountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName)) diff --git a/sei-cosmos/x/genutil/gentx_test.go b/sei-cosmos/x/genutil/gentx_test.go index 993c17b424..3dd0dc8308 100644 --- a/sei-cosmos/x/genutil/gentx_test.go +++ b/sei-cosmos/x/genutil/gentx_test.go @@ -260,7 +260,7 @@ func (suite *GenTxTestSuite) TestDeliverGenTxs() { tc.malleate() - ctx := suite.app.GetContextForDeliverTx([]byte{}) + ctx := suite.app.GetContextForDeliverTx([]byte{}).WithIsGenesis(true) if tc.expPass { suite.Require().NotPanics(func() { genutil.DeliverGenTxs( diff --git a/sei-cosmos/x/gov/client/cli/tx_test.go b/sei-cosmos/x/gov/client/cli/tx_test.go index b6957e65cb..63a0beed3f 100644 --- a/sei-cosmos/x/gov/client/cli/tx_test.go +++ b/sei-cosmos/x/gov/client/cli/tx_test.go @@ -3,12 +3,12 @@ package cli_test import ( "bytes" "fmt" - "github.com/gogo/protobuf/proto" - abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - rpcclientmock "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" "io" "testing" + "github.com/gogo/protobuf/proto" + abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/client" "github.com/sei-protocol/sei-chain/sei-cosmos/client/flags" "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keyring" @@ -39,12 +39,11 @@ func TestCLITestSuite(t *testing.T) { func (s *CLITestSuite) SetupSuite() { s.encCfg = testutilmod.MakeTestEncodingConfig(gov.AppModuleBasic{}) s.kr = keyring.NewInMemory() - mockRPC := rpcclientmock.New() s.baseCtx = client.Context{}. WithKeyring(s.kr). WithTxConfig(s.encCfg.TxConfig). WithCodec(s.encCfg.Codec). - WithClient(clitestutil.MockTendermintRPC{Client: mockRPC}). + WithClient(clitestutil.MockTendermintRPC{}). WithAccountRetriever(client.MockAccountRetriever{}). WithOutput(io.Discard). WithChainID("test-chain") @@ -54,7 +53,7 @@ func (s *CLITestSuite) SetupSuite() { bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) c := clitestutil.NewMockTendermintRPC(abci.ResponseQuery{ Value: bz, - }, mockRPC) + }, nil) return s.baseCtx.WithClient(c) } s.clientCtx = ctxGen().WithOutput(&outBuf) diff --git a/sei-cosmos/x/gov/client/utils/query_test.go b/sei-cosmos/x/gov/client/utils/query_test.go index e72e23457b..765abce3e4 100644 --- a/sei-cosmos/x/gov/client/utils/query_test.go +++ b/sei-cosmos/x/gov/client/utils/query_test.go @@ -5,7 +5,6 @@ import ( "regexp" "testing" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" ctypes "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" "github.com/stretchr/testify/require" @@ -20,7 +19,7 @@ import ( type TxSearchMock struct { txConfig client.TxConfig - mock.Client + client.Client txs []tmtypes.Tx } diff --git a/sei-cosmos/x/gov/genesis_test.go b/sei-cosmos/x/gov/genesis_test.go index c1d5646f8e..176735c39e 100644 --- a/sei-cosmos/x/gov/genesis_test.go +++ b/sei-cosmos/x/gov/genesis_test.go @@ -70,11 +70,10 @@ func TestImportExportQueues(t *testing.T) { db := dbm.NewMemDB() app2 := seiapp.SetupWithDB(t, db, false, false, false) - app2.InitChain( - context.Background(), &abci.RequestInitChain{ - ConsensusParams: seiapp.DefaultConsensusParams, - AppStateBytes: stateBytes, - }, + app2.InitChain(&abci.RequestInitChain{ + ConsensusParams: seiapp.DefaultConsensusParams, + AppStateBytes: stateBytes, + }, ) app2.Commit(context.Background()) diff --git a/sei-cosmos/x/gov/module_test.go b/sei-cosmos/x/gov/module_test.go index 2fc87fc2e4..44704805c6 100644 --- a/sei-cosmos/x/gov/module_test.go +++ b/sei-cosmos/x/gov/module_test.go @@ -1,7 +1,6 @@ package gov_test import ( - "context" "testing" abcitypes "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -17,11 +16,10 @@ func TestItCreatesModuleAccountOnInitBlock(t *testing.T) { app := seiapp.Setup(t, false, false, false) ctx := app.BaseApp.NewContext(false, tmproto.Header{}) - app.InitChain( - context.Background(), &abcitypes.RequestInitChain{ - AppStateBytes: []byte("{}"), - ChainId: "test-chain-id", - }, + app.InitChain(&abcitypes.RequestInitChain{ + AppStateBytes: []byte("{}"), + ChainId: "test-chain-id", + }, ) acc := app.AccountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName)) diff --git a/sei-cosmos/x/staking/module_test.go b/sei-cosmos/x/staking/module_test.go index 705c40bdb6..1331ff27cf 100644 --- a/sei-cosmos/x/staking/module_test.go +++ b/sei-cosmos/x/staking/module_test.go @@ -1,7 +1,6 @@ package staking_test import ( - "context" "testing" abcitypes "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -17,11 +16,10 @@ func TestItCreatesModuleAccountOnInitBlock(t *testing.T) { app := seiapp.Setup(t, false, false, false) ctx := app.BaseApp.NewContext(false, tmproto.Header{}) - app.InitChain( - context.Background(), &abcitypes.RequestInitChain{ - AppStateBytes: []byte("{}"), - ChainId: "test-chain-id", - }, + app.InitChain(&abcitypes.RequestInitChain{ + AppStateBytes: []byte("{}"), + ChainId: "test-chain-id", + }, ) acc := app.AccountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.BondedPoolName)) diff --git a/sei-cosmos/x/upgrade/abci_test.go b/sei-cosmos/x/upgrade/abci_test.go index 40f2c581c6..252c123b89 100644 --- a/sei-cosmos/x/upgrade/abci_test.go +++ b/sei-cosmos/x/upgrade/abci_test.go @@ -40,7 +40,7 @@ func setupTest(t *testing.T, height int64) TestSuite { if err != nil { panic(err) } - app.InitChain(t.Context(), &abci.RequestInitChain{AppStateBytes: stateBytes}) + app.InitChain(&abci.RequestInitChain{AppStateBytes: stateBytes}) s.keeper = app.UpgradeKeeper s.ctx = app.BaseApp.NewContext(false, tmproto.Header{Height: height, Time: time.Now()}) diff --git a/sei-ibc-go/testing/simapp/test_helpers.go b/sei-ibc-go/testing/simapp/test_helpers.go index b0ffebf156..eb27442f60 100644 --- a/sei-ibc-go/testing/simapp/test_helpers.go +++ b/sei-ibc-go/testing/simapp/test_helpers.go @@ -106,12 +106,10 @@ func Setup(isCheckTx bool) *SimApp { } // Initialize the chain - _, err = app.InitChain( - context.Background(), - &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - AppStateBytes: stateBytes, - }, + _, err = app.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + AppStateBytes: stateBytes, + }, ) if err != nil { panic(err) @@ -186,13 +184,11 @@ func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs require.NoError(t, err) // init chain will set the validator set and initialize the genesis accounts - _, err = app.InitChain( - t.Context(), - &abci.RequestInitChain{ - ChainId: chainID, - ConsensusParams: DefaultConsensusParams, - AppStateBytes: stateBytes, - }, + _, err = app.InitChain(&abci.RequestInitChain{ + ChainId: chainID, + ConsensusParams: DefaultConsensusParams, + AppStateBytes: stateBytes, + }, ) require.NoError(t, err) diff --git a/sei-tendermint/AGENTS.md b/sei-tendermint/AGENTS.md index 855348511e..ee38c7c81e 100644 --- a/sei-tendermint/AGENTS.md +++ b/sei-tendermint/AGENTS.md @@ -2,7 +2,15 @@ Within sei-tendermint subdirectory * sei-tendermint is a root of the go module, but it is not the root of the repo. When refactoring check references across of the whole repo. * use sei-tendermint/libs/utils.Option for optional values, passing nil as function value is not allowed, unless explicitly documented (except for maps and slices, for which nil is a valid empty value). -* use sei-tendermint/libs/utils/scope.Run for structured concurrency (instead of waitgroup) +* struct fields are assumed to be non-nil by default. Do not add defensive nil-checks in internal logic. + External data, like proto generated code for example, might require nil checks. +* use utils.Recv/utils.Send instead of select { case <-ctx.Done(); ... } +* use strongly typed utils/require asserts instead of testify/require +* use sei-tendermint/libs/utils/scope.Run for structured concurrency (instead of waitgroup). + * Don't spawn goroutines with plain "go func(){...}" + * using testing.T assertions (inclusing those from "require" library) is not allowed from non-main test goroutine. + In particular it is not allowed in Scope.Run() and goroutines spawned via Scope.Spawn (and related ones). On assertion error, return an error + or panic directly. If you are unsure if a test helper would be called from non-main test goroutine, dont use testing.T assertions. * use new golang features wherever possible, which is at least 1.25. In particular: * modern range iteration ("for range N", to iterate N times) * avoid capturing loop iterators ("tc := tc" is not needed) @@ -15,16 +23,15 @@ Within sei-tendermint subdirectory you can use private fields directly in case it is not accessible via public api. * when writing tests, focus on asserting the publically visible properties, especially the API contract. Avoid asserting implementation details. * Avoid sleeping and active polling in tests. Prefer waiting for updates (for example by using sei-tendermint/libs/utils.Watch or AtomicWatch). +* Do not introduce artificial timeouts in tests because they are a source of flakiness. Let the test hang and make the user run go tests with explicit + timeout instead. * Avoid checking human readable error messages in tests. If programatic error type check is requested, use errors.Is/As/AsType instead. * After introducing changes, you may Use `go test --count=0` to quickly check if tests compile. You may run `go test` to actually run the tests afterwards. Prefer running modified tests selectively and run the whole test suite only if requested or looking for failures. -* Proto fields: use `optional` for all scalar and message fields (required for protobuf3 presence - detection and wireguard bounds checking). Annotate semantically required fields with `// required` +* Proto fields: For hashable messages, use explicit "optional" for all signular fields and annotate semantically required fields with `// required` and truly optional fields with `// optional`. Example: optional uint64 index = 1; // required optional AppProposal app = 4; // optional - Required fields must be nil-checked at the boundary (constructor and proto Decode) and trusted - everywhere else — do not add defensive nil-checks in internal logic. * Avoid removing comments and logs which are not obviously obsolete. Keep the original wording, only fixing mistakes or obsolete parts. * TestRng instance should be one per test, constructed directly in the test function. In case of nested/table tests, each nested test should create its own instance. Use TestRng.Split() (before spawning) if you need to pass entropy source to a spawned goroutine diff --git a/sei-tendermint/abci/example/kvstore/kvstore.go b/sei-tendermint/abci/example/kvstore/kvstore.go index c9b142d55e..988ee185a6 100644 --- a/sei-tendermint/abci/example/kvstore/kvstore.go +++ b/sei-tendermint/abci/example/kvstore/kvstore.go @@ -101,11 +101,11 @@ func NewProxy() *proxy.Proxy { return proxy.New(NewApplication()) } -func (app *Application) InitChain(_ context.Context, req *types.RequestInitChain) (*types.ResponseInitChain, error) { +func (app *Application) InitChain(req *types.RequestInitChain) (*types.ResponseInitChain, error) { return &types.ResponseInitChain{Validators: app.Validators()}, nil } -func (app *Application) Info(_ context.Context, req *types.RequestInfo) (*types.ResponseInfo, error) { +func (app *Application) Info() *types.ResponseInfo { app.mu.Lock() defer app.mu.Unlock() return &types.ResponseInfo{ @@ -114,7 +114,7 @@ func (app *Application) Info(_ context.Context, req *types.RequestInfo) (*types. AppVersion: ProtocolVersion, LastBlockHeight: app.state.Height, LastBlockAppHash: app.state.AppHash, - }, nil + } } func (app *Application) LastBlockHeight() int64 { diff --git a/sei-tendermint/abci/example/kvstore/kvstore_test.go b/sei-tendermint/abci/example/kvstore/kvstore_test.go index 6297bb1483..55fc62a0ce 100644 --- a/sei-tendermint/abci/example/kvstore/kvstore_test.go +++ b/sei-tendermint/abci/example/kvstore/kvstore_test.go @@ -32,8 +32,7 @@ func testKVStore(ctx context.Context, t *testing.T, app types.Application, tx [] _, err = app.Commit(ctx) require.NoError(t, err) - info, err := app.Info(ctx, &types.RequestInfo{}) - require.NoError(t, err) + info := app.Info() require.NotZero(t, info.LastBlockHeight) // make sure query is fine @@ -95,7 +94,7 @@ func TestValUpdates(t *testing.T) { nInit := 5 vals := RandVals(total) // initialize with the first nInit - _, err := kvstore.InitChain(ctx, &types.RequestInitChain{}) + _, err := kvstore.InitChain(&types.RequestInitChain{}) if err != nil { t.Fatal(err) } diff --git a/sei-tendermint/abci/types/application.go b/sei-tendermint/abci/types/application.go index 5db2209dd2..f16dd68c02 100644 --- a/sei-tendermint/abci/types/application.go +++ b/sei-tendermint/abci/types/application.go @@ -5,6 +5,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" + + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" ) // Application is an interface that enables any finite, deterministic state machine @@ -13,7 +15,7 @@ import ( //go:generate ../../scripts/mockery_generate.sh Application type Application interface { // Info/Query Connection - Info(context.Context, *RequestInfo) (*ResponseInfo, error) // Return application info + Info() *ResponseInfo // Return application info Query(context.Context, *RequestQuery) (*ResponseQuery, error) // Query for state GetValidators() []ValidatorUpdate // LastBlockHeight returns the height of the most recently committed @@ -28,7 +30,8 @@ type Application interface { EvmBalance(common.Address, []byte) uint256.Int // Consensus Connection - InitChain(context.Context, *RequestInitChain) (*ResponseInitChain, error) // Initialize blockchain w validators/other info from TendermintCore + InitChain(*RequestInitChain) (*ResponseInitChain, error) // Initialize blockchain w validators/other info from TendermintCore + InitLastHeader(lastHeader *tmproto.Header) ProcessProposal(context.Context, *RequestProcessProposal) (*ResponseProcessProposal, error) // Commit the state and return the application Merkle root hash Commit(context.Context) (*ResponseCommit, error) @@ -49,8 +52,8 @@ var _ Application = BaseApplication{} type BaseApplication struct{} -func (BaseApplication) Info(_ context.Context, req *RequestInfo) (*ResponseInfo, error) { - return &ResponseInfo{}, nil +func (BaseApplication) Info() *ResponseInfo { + return &ResponseInfo{} } func (BaseApplication) GetValidators() []ValidatorUpdate { return nil } func (BaseApplication) LastBlockHeight() int64 { return 0 } @@ -67,7 +70,9 @@ func (BaseApplication) Query(_ context.Context, req *RequestQuery) (*ResponseQue return &ResponseQuery{Code: CodeTypeOK}, nil } -func (BaseApplication) InitChain(_ context.Context, req *RequestInitChain) (*ResponseInitChain, error) { +func (BaseApplication) InitLastHeader(lastHeader *tmproto.Header) {} + +func (BaseApplication) InitChain(req *RequestInitChain) (*ResponseInitChain, error) { return &ResponseInitChain{}, nil } diff --git a/sei-tendermint/abci/types/mocks/application.go b/sei-tendermint/abci/types/mocks/application.go index c474b32770..e36908bfad 100644 --- a/sei-tendermint/abci/types/mocks/application.go +++ b/sei-tendermint/abci/types/mocks/application.go @@ -9,6 +9,8 @@ import ( mock "github.com/stretchr/testify/mock" + tenderminttypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + types "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" uint256 "github.com/holiman/uint256" @@ -217,39 +219,29 @@ func (_m *Application) GetValidators() []types.ValidatorUpdate { return r0 } -// Info provides a mock function with given fields: _a0, _a1 -func (_m *Application) Info(_a0 context.Context, _a1 *types.RequestInfo) (*types.ResponseInfo, error) { - ret := _m.Called(_a0, _a1) +// Info provides a mock function with no fields +func (_m *Application) Info() *types.ResponseInfo { + ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Info") } var r0 *types.ResponseInfo - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *types.RequestInfo) (*types.ResponseInfo, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *types.RequestInfo) *types.ResponseInfo); ok { - r0 = rf(_a0, _a1) + if rf, ok := ret.Get(0).(func() *types.ResponseInfo); ok { + r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*types.ResponseInfo) } } - if rf, ok := ret.Get(1).(func(context.Context, *types.RequestInfo) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } -// InitChain provides a mock function with given fields: _a0, _a1 -func (_m *Application) InitChain(_a0 context.Context, _a1 *types.RequestInitChain) (*types.ResponseInitChain, error) { - ret := _m.Called(_a0, _a1) +// InitChain provides a mock function with given fields: _a0 +func (_m *Application) InitChain(_a0 *types.RequestInitChain) (*types.ResponseInitChain, error) { + ret := _m.Called(_a0) if len(ret) == 0 { panic("no return value specified for InitChain") @@ -257,19 +249,19 @@ func (_m *Application) InitChain(_a0 context.Context, _a1 *types.RequestInitChai var r0 *types.ResponseInitChain var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *types.RequestInitChain) (*types.ResponseInitChain, error)); ok { - return rf(_a0, _a1) + if rf, ok := ret.Get(0).(func(*types.RequestInitChain) (*types.ResponseInitChain, error)); ok { + return rf(_a0) } - if rf, ok := ret.Get(0).(func(context.Context, *types.RequestInitChain) *types.ResponseInitChain); ok { - r0 = rf(_a0, _a1) + if rf, ok := ret.Get(0).(func(*types.RequestInitChain) *types.ResponseInitChain); ok { + r0 = rf(_a0) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*types.ResponseInitChain) } } - if rf, ok := ret.Get(1).(func(context.Context, *types.RequestInitChain) error); ok { - r1 = rf(_a0, _a1) + if rf, ok := ret.Get(1).(func(*types.RequestInitChain) error); ok { + r1 = rf(_a0) } else { r1 = ret.Error(1) } @@ -277,6 +269,11 @@ func (_m *Application) InitChain(_a0 context.Context, _a1 *types.RequestInitChai return r0, r1 } +// InitLastHeader provides a mock function with given fields: lastHeader +func (_m *Application) InitLastHeader(lastHeader *tenderminttypes.Header) { + _m.Called(lastHeader) +} + // LastBlockHeight provides a mock function with no fields func (_m *Application) LastBlockHeight() int64 { ret := _m.Called() diff --git a/sei-tendermint/cmd/tendermint/commands/gen_autobahn_config.go b/sei-tendermint/cmd/tendermint/commands/gen_autobahn_config.go index 13736e876d..111371ec0c 100644 --- a/sei-tendermint/cmd/tendermint/commands/gen_autobahn_config.go +++ b/sei-tendermint/cmd/tendermint/commands/gen_autobahn_config.go @@ -83,8 +83,8 @@ Output is written to the file specified by --output.`, cfg := config.AutobahnFileConfig{ Validators: validators, - MaxTxsPerBlock: 5_000, - AllowEmptyBlocks: true, + MaxTxsPerBlock: 2_000, + AllowEmptyBlocks: false, BlockInterval: utils.Duration(400 * time.Millisecond), ViewTimeout: utils.Duration(1500 * time.Millisecond), DialInterval: utils.Duration(10 * time.Second), diff --git a/sei-tendermint/internal/consensus/byzantine_test.go b/sei-tendermint/internal/consensus/byzantine_test.go index fa0bd68866..8ea8427c7d 100644 --- a/sei-tendermint/internal/consensus/byzantine_test.go +++ b/sei-tendermint/internal/consensus/byzantine_test.go @@ -70,7 +70,7 @@ package consensus // ensureDir(t, path.Dir(thisConfig.Consensus.WalFile()), 0700) // dir for wal // app := kvstore.NewApplication() // vals := types.TM2PB.ValidatorUpdates(state.Validators) -// _, err = app.InitChain(ctx, &abci.RequestInitChain{Validators: vals}) +// _, err = app.InitChain(&abci.RequestInitChain{Validators: vals}) // require.NoError(t, err) // // blockDB := dbm.NewMemDB() diff --git a/sei-tendermint/internal/consensus/common_test.go b/sei-tendermint/internal/consensus/common_test.go index 39379d3932..32eb988029 100644 --- a/sei-tendermint/internal/consensus/common_test.go +++ b/sei-tendermint/internal/consensus/common_test.go @@ -988,7 +988,7 @@ func makeConsensusState( closeFuncs = append(closeFuncs, app.Close) vals := types.TM2PB.ValidatorUpdates(state.Validators) - _, err = app.InitChain(ctx, &abci.RequestInitChain{}) + _, err = app.InitChain(&abci.RequestInitChain{}) require.NoError(t, err) app.SetValidators(vals) @@ -1052,7 +1052,7 @@ func randConsensusNetWithPeers( app := kvstore.NewApplication() vals := types.TM2PB.ValidatorUpdates(state.Validators) state.Version.Consensus.App = kvstore.ProtocolVersion - _, err = app.InitChain(ctx, &abci.RequestInitChain{}) + _, err = app.InitChain(&abci.RequestInitChain{}) require.NoError(t, err) app.SetValidators(vals) // sm.SaveState(stateDB,state) //height 1's validatorsInfo already saved in LoadStateFromDBOrGenesisDoc above diff --git a/sei-tendermint/internal/consensus/mempool_test.go b/sei-tendermint/internal/consensus/mempool_test.go index 3487c01a1a..caea073894 100644 --- a/sei-tendermint/internal/consensus/mempool_test.go +++ b/sei-tendermint/internal/consensus/mempool_test.go @@ -293,11 +293,11 @@ func NewCounterApplication() *CounterApplication { return &CounterApplication{} } -func (app *CounterApplication) Info(_ context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { +func (app *CounterApplication) Info() *abci.ResponseInfo { app.mu.Lock() defer app.mu.Unlock() - return &abci.ResponseInfo{Data: fmt.Sprintf("txs:%v", app.txCount)}, nil + return &abci.ResponseInfo{Data: fmt.Sprintf("txs:%v", app.txCount)} } func (app *CounterApplication) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { diff --git a/sei-tendermint/internal/consensus/reactor_test.go b/sei-tendermint/internal/consensus/reactor_test.go index ced7f627a2..124815e3f0 100644 --- a/sei-tendermint/internal/consensus/reactor_test.go +++ b/sei-tendermint/internal/consensus/reactor_test.go @@ -261,7 +261,7 @@ func TestReactorWithEvidence(t *testing.T) { app := kvstore.NewApplication() vals := types.TM2PB.ValidatorUpdates(state.Validators) app.SetValidators(vals) - _, err = app.InitChain(ctx, &abci.RequestInitChain{}) + _, err = app.InitChain(&abci.RequestInitChain{}) require.NoError(t, err) pv := privVals[i] diff --git a/sei-tendermint/internal/consensus/replay.go b/sei-tendermint/internal/consensus/replay.go index 175be9c573..6fc1b2b552 100644 --- a/sei-tendermint/internal/consensus/replay.go +++ b/sei-tendermint/internal/consensus/replay.go @@ -15,7 +15,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" sm "github.com/sei-protocol/sei-chain/sei-tendermint/internal/state" "github.com/sei-protocol/sei-chain/sei-tendermint/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/version" "github.com/sei-protocol/seilog" ) @@ -149,11 +148,7 @@ func (h *Handshaker) NBlocks() int { // TODO: retry the handshake/replay if it fails ? func (h *Handshaker) Handshake(ctx context.Context, app *proxy.Proxy) error { - res, err := app.Info(ctx, &version.RequestInfo) - if err != nil { - return fmt.Errorf("error calling Info: %w", err) - } - + res := app.Info() blockHeight := res.LastBlockHeight if blockHeight < 0 { return fmt.Errorf("got a negative last block height (%d) from the app", blockHeight) @@ -174,7 +169,7 @@ func (h *Handshaker) Handshake(ctx context.Context, app *proxy.Proxy) error { } // Replay blocks up to the latest in the blockstore. - _, err = h.ReplayBlocks(ctx, h.initialState, appHash, blockHeight, app) + _, err := h.ReplayBlocks(ctx, h.initialState, appHash, blockHeight, app) if err != nil { return fmt.Errorf("error on replay: %w", err) } @@ -204,7 +199,7 @@ func (h *Handshaker) ReplayBlocks( // If appBlockHeight == 0 it means that we are at genesis and hence should send InitChain. if appBlockHeight == 0 { - res, err := app.InitChain(ctx, h.genDoc.ToRequestInitChain()) + res, err := app.InitChain(h.genDoc.ToRequestInitChain()) if err != nil { return nil, err } diff --git a/sei-tendermint/internal/consensus/replay_test.go b/sei-tendermint/internal/consensus/replay_test.go index e114a73d1a..aa55bc68e5 100644 --- a/sei-tendermint/internal/consensus/replay_test.go +++ b/sei-tendermint/internal/consensus/replay_test.go @@ -704,10 +704,7 @@ func testHandshakeReplay( require.NoError(t, err, "Error on abci handshake") // get the latest app hash from the app - res, err := proxyApp.Info(ctx, &abci.RequestInfo{Version: ""}) - if err != nil { - t.Fatal(err) - } + res := proxyApp.Info() // the app hash should be synced up if !bytes.Equal(latestAppHash, res.LastBlockAppHash) { @@ -770,7 +767,7 @@ func buildAppStateFromChain( proxyApp := proxy.New(appClient) mempool := newReplayTxMempool(proxyApp) state.Version.Consensus.App = kvstore.ProtocolVersion // simulate handshake, receive app version - _, err := appClient.InitChain(ctx, &abci.RequestInitChain{}) + _, err := appClient.InitChain(&abci.RequestInitChain{}) require.NoError(t, err) require.NoError(t, stateStore.Save(state)) // save height 1's validatorsInfo @@ -814,7 +811,7 @@ func buildTMStateFromChain( app := newApp(types.TM2PB.ValidatorUpdates(state.Validators)) proxyApp := proxy.New(app) state.Version.Consensus.App = kvstore.ProtocolVersion // simulate handshake, receive app version - _, err := app.InitChain(ctx, &abci.RequestInitChain{}) + _, err := app.InitChain(&abci.RequestInitChain{}) require.NoError(t, err) require.NoError(t, stateStore.Save(state)) @@ -1140,6 +1137,6 @@ type initChainApp struct { vals []abci.ValidatorUpdate } -func (ica *initChainApp) InitChain(_ context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { +func (ica *initChainApp) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { return &abci.ResponseInitChain{Validators: ica.vals}, nil } diff --git a/sei-tendermint/internal/consensus/state_empty_valset_test.go b/sei-tendermint/internal/consensus/state_empty_valset_test.go index f13503e277..19d897c5dc 100644 --- a/sei-tendermint/internal/consensus/state_empty_valset_test.go +++ b/sei-tendermint/internal/consensus/state_empty_valset_test.go @@ -23,8 +23,6 @@ import ( // leaving the round state holding an empty validator set. func newStateFromEmptyGenesisValidators(t *testing.T) *State { t.Helper() - ctx := t.Context() - cfg := configSetup(t) genDoc := factory.GenesisDoc(cfg, time.Now(), nil, factory.ConsensusParams()) state, err := sm.MakeGenesisState(genDoc) @@ -37,7 +35,7 @@ func newStateFromEmptyGenesisValidators(t *testing.T) *State { app := kvstore.NewApplication() t.Cleanup(func() { _ = app.Close() }) - _, err = app.InitChain(ctx, &abci.RequestInitChain{}) + _, err = app.InitChain(&abci.RequestInitChain{}) require.NoError(t, err) pv := loadPrivValidator(thisConfig) diff --git a/sei-tendermint/internal/p2p/giga/pool.go b/sei-tendermint/internal/p2p/giga/pool.go index 9c5917f734..6c29d1679f 100644 --- a/sei-tendermint/internal/p2p/giga/pool.go +++ b/sei-tendermint/internal/p2p/giga/pool.go @@ -66,6 +66,15 @@ func (p *Pool[K, V]) InsertAndRun(ctx context.Context, key K, val V, task func(c return err } +func (p *Pool[K, V]) Get(key K) (V, bool) { + for inner := range p.inner.Lock() { + if entry, ok := inner.byKey[key]; ok { + return entry.val, true + } + } + return utils.Zero[V](), false +} + func (p *Pool[K, V]) RunForEach(ctx context.Context, task func(context.Context, V) error) error { return scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { var next poolEntry[V] diff --git a/sei-tendermint/internal/p2p/giga_router_common.go b/sei-tendermint/internal/p2p/giga_router_common.go index 9be48343c3..8987d96102 100644 --- a/sei-tendermint/internal/p2p/giga_router_common.go +++ b/sei-tendermint/internal/p2p/giga_router_common.go @@ -25,7 +25,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/tcp" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" "github.com/sei-protocol/sei-chain/sei-tendermint/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/version" ) // maxInboundFullnodePeers caps GigaRouterCommonConfig.MaxInboundFullnodePeers. @@ -351,10 +350,7 @@ func (r *gigaRouterCommon) runExecute(ctx context.Context) error { app := r.app - info, err := app.Info(ctx, &version.RequestInfo) - if err != nil { - return fmt.Errorf("App.Info(): %w", err) - } + info := app.Info() last, ok := utils.SafeCast[atypes.GlobalBlockNumber](info.LastBlockHeight) if !ok { return fmt.Errorf("invalid info.LastBlockHeight = %v", info.LastBlockHeight) @@ -369,7 +365,7 @@ func (r *gigaRouterCommon) runExecute(ctx context.Context) error { // Re-entering on restart (crashed after InitChain, before first // Commit) is safe — nothing was committed, so it behaves as a // fresh init. - if _, err := app.InitChain(ctx, r.cfg.GenDoc.ToRequestInitChain()); err != nil { + if _, err := app.InitChain(r.cfg.GenDoc.ToRequestInitChain()); err != nil { return fmt.Errorf("App.InitChain(): %w", err) } var ok bool @@ -378,6 +374,17 @@ func (r *gigaRouterCommon) runExecute(ctx context.Context) error { return fmt.Errorf("invalid GenDoc.InitialHeight = %v", r.cfg.GenDoc.InitialHeight) } } else { + b, err := r.data.GlobalBlock(ctx, last) + if err != nil { + return fmt.Errorf("r.data.GlobalBlock(): %w", err) + } + app.InitLastHeader((&types.Header{ + ChainID: r.cfg.GenDoc.ChainID, + Height: int64(b.GlobalNumber), // nolint:gosec // different representations of the same value + Time: b.Timestamp, + // TODO: for consistency we should also set proposerAddress here, + // but this is a placeholder solution so maybe we don't care. + }).ToProto()) // Re-commit the last finalized block's app hash to the equivocation guard before re-proposing it // for AppQC voting (PushAppHash below), mirroring executeBlock's commit-before-PushAppHash // ordering. On a normal restart this idempotently matches the hash recorded when `last` was diff --git a/sei-tendermint/internal/p2p/giga_router_testhelper_test.go b/sei-tendermint/internal/p2p/giga_router_testhelper_test.go index f63de09719..07715f534c 100644 --- a/sei-tendermint/internal/p2p/giga_router_testhelper_test.go +++ b/sei-tendermint/internal/p2p/giga_router_testhelper_test.go @@ -60,23 +60,23 @@ func (a *testApp) GetValidators() []abci.ValidatorUpdate { panic("unreachable") } -func (a *testApp) Info(_ context.Context, _ *abci.RequestInfo) (*abci.ResponseInfo, error) { +func (a *testApp) Info() *abci.ResponseInfo { for state := range a.state.Lock() { init, ok := state.Init.Get() if !ok { - return &abci.ResponseInfo{}, nil + return &abci.ResponseInfo{} } if len(state.Blocks) == 0 { // Match the real SDK: InitChain without Commit leaves LastBlockHeight=0. return &abci.ResponseInfo{ LastBlockHeight: 0, LastBlockAppHash: slices.Clone(state.AppHash[:]), - }, nil + } } return &abci.ResponseInfo{ LastBlockHeight: init.InitialHeight + int64(len(state.Blocks)) - 1, LastBlockAppHash: slices.Clone(state.AppHash[:]), - }, nil + } } panic("unreachable") } @@ -102,7 +102,7 @@ func (a *testApp) CheckTx(context.Context, *abci.RequestCheckTxV2) *abci.Respons } } -func (a *testApp) InitChain(_ context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { +func (a *testApp) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { for state, ctrl := range a.state.Lock() { if state.Init.IsPresent() { return nil, fmt.Errorf("chain already initialized") @@ -217,7 +217,7 @@ func TestInitChainCommitThenFinalize(t *testing.T) { appState := testAppStateJSON(rng) // InitChain - _, err := app.InitChain(ctx, &abci.RequestInitChain{ + _, err := app.InitChain(&abci.RequestInitChain{ InitialHeight: initialHeight, AppStateBytes: appState, }) @@ -227,8 +227,7 @@ func TestInitChainCommitThenFinalize(t *testing.T) { // using the deliverState set up by InitChain. // Verify app reports correct height after InitChain (no blocks yet) - info, err := app.Info(ctx, &abci.RequestInfo{}) - require.NoError(t, err) + info := app.Info() require.Equal(t, int64(0), info.LastBlockHeight, "testApp should report 0 after InitChain with no committed blocks (matches real SDK)") @@ -247,8 +246,7 @@ func TestInitChainCommitThenFinalize(t *testing.T) { require.NoError(t, err) // Verify height advanced - info, err = app.Info(ctx, &abci.RequestInfo{}) - require.NoError(t, err) + info = app.Info() require.Equal(t, initialHeight, info.LastBlockHeight, "testApp should report InitialHeight after 1 block") } diff --git a/sei-tendermint/internal/p2p/giga_router_validator.go b/sei-tendermint/internal/p2p/giga_router_validator.go index 9fe39e6dbf..7f77113e6e 100644 --- a/sei-tendermint/internal/p2p/giga_router_validator.go +++ b/sei-tendermint/internal/p2p/giga_router_validator.go @@ -87,11 +87,17 @@ func (r *gigaValidatorRouter) Run(ctx context.Context) error { } // EvmProxy on the validator returns None when the sender's shard owner is -// us (handle locally via mempool, no HTTP round-trip to self). +// us (handle locally via mempool, no HTTP round-trip to self). For remote +// shards, we proxy only while the target validator is currently connected; +// otherwise we keep the tx local as a best-effort availability heuristic. func (r *gigaValidatorRouter) EvmProxy(sender common.Address) utils.Option[*url.URL] { shardValidator := r.data.Registry().LatestEpoch().Committee().EvmShard(sender) if r.validatorKey == shardValidator { return utils.None[*url.URL]() } - return utils.Some(r.cfg.ValidatorAddrs[shardValidator].EVMRPC) + target := r.cfg.ValidatorAddrs[shardValidator] + if _, ok := r.poolOut.Get(target.Key); !ok { + return utils.None[*url.URL]() + } + return utils.Some(target.EVMRPC) } diff --git a/sei-tendermint/internal/p2p/giga_router_validator_test.go b/sei-tendermint/internal/p2p/giga_router_validator_test.go index 1ee7e80f8a..d1d6fad93b 100644 --- a/sei-tendermint/internal/p2p/giga_router_validator_test.go +++ b/sei-tendermint/internal/p2p/giga_router_validator_test.go @@ -3,6 +3,7 @@ package p2p import ( "context" "fmt" + "maps" "net/url" "testing" "time" @@ -14,6 +15,8 @@ import ( atypes "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/producer" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/conn" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/giga" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" @@ -199,8 +202,7 @@ func TestGigaRouter_EvmProxy(t *testing.T) { addrs := map[atypes.PublicKey]GigaNodeAddr{} urlByValidator := map[atypes.PublicKey]*url.URL{} // NewGigaRouter requires EVMRPC on every committee member in both - // validator and fullnode modes; the missing-URL branch of EvmProxy is - // unreachable. + // validator and fullnode modes. for i, validatorKey := range validatorKeys { nodeKey := makeKey(rng) nodeKeys = append(nodeKeys, nodeKey) @@ -244,34 +246,74 @@ func TestGigaRouter_EvmProxy(t *testing.T) { localURL, ok := urlByValidator[localValidator] require.True(t, ok) - expectedRemoteURLs := map[string]struct{}{} - for validator, rpcURL := range urlByValidator { - if validator == localValidator { - continue + err = scope.Run(t.Context(), func(ctx context.Context, s scope.Scope) error { + connectedRemote := map[atypes.PublicKey]struct{}{} + expectedRemoteURLs := map[string]struct{}{} + type noOpAPI = rpc.Client[giga.API] + for validator, addr := range addrs { + if validator == localValidator { + continue + } + if len(connectedRemote) >= 3 { + break + } + connectedRemote[validator] = struct{}{} + expectedRemoteURLs[addr.EVMRPC.String()] = struct{}{} + key := addr.Key + ready := make(chan struct{}) + s.SpawnBgNamed(fmt.Sprintf("poolOut[%s]", key), func() error { + var client noOpAPI + return utils.IgnoreCancel(router.poolOut.InsertAndRun(ctx, key, client, func(ctx context.Context) error { + close(ready) + <-ctx.Done() + return nil + })) + }) + if _, _, err := utils.RecvOrClosed(ctx, ready); err != nil { + return err + } } - expectedRemoteURLs[rpcURL.String()] = struct{}{} - } - returnedRemoteURLs := map[string]struct{}{} - for range 200 { - sender := common.BytesToAddress(utils.GenBytes(rng, common.AddressLength)) - shardValidator := router.data.Registry().LatestEpoch().Committee().EvmShard(sender) + returnedRemoteURLs := map[string]struct{}{} + seenDisconnected := false + for range 400 { + sender := common.BytesToAddress(utils.GenBytes(rng, common.AddressLength)) + shardValidator := router.data.Registry().LatestEpoch().Committee().EvmShard(sender) - proxyURL, ok := router.EvmProxy(sender).Get() - expectedURL := urlByValidator[shardValidator] + proxyURL, ok := router.EvmProxy(sender).Get() + expectedURL := urlByValidator[shardValidator] - if shardValidator == localValidator { - // Self-shard: validator short-circuits to local mempool. - require.False(t, ok) - require.Nil(t, proxyURL) - } else { - require.True(t, ok) - require.NotNil(t, proxyURL) - require.Equal(t, expectedURL.String(), proxyURL.String()) - require.NotEqual(t, localURL.String(), proxyURL.String()) - returnedRemoteURLs[proxyURL.String()] = struct{}{} + if shardValidator == localValidator { + // Self-shard: validator short-circuits to local mempool. + if ok || proxyURL != nil { + return fmt.Errorf("expected local shard %s to avoid proxying", shardValidator) + } + } else if _, connected := connectedRemote[shardValidator]; !connected { + if ok || proxyURL != nil { + return fmt.Errorf("expected disconnected shard %s to avoid proxying", shardValidator) + } + seenDisconnected = true + } else { + if !ok || proxyURL == nil { + return fmt.Errorf("expected connected shard %s to proxy", shardValidator) + } + if got := proxyURL.String(); got != expectedURL.String() { + return fmt.Errorf("proxy url = %s, want %s", got, expectedURL.String()) + } + if proxyURL.String() == localURL.String() { + return fmt.Errorf("connected remote shard %s unexpectedly proxied to local URL", shardValidator) + } + returnedRemoteURLs[proxyURL.String()] = struct{}{} + } } - } - require.Equal(t, expectedRemoteURLs, returnedRemoteURLs) + if !maps.Equal(expectedRemoteURLs, returnedRemoteURLs) { + return fmt.Errorf("returned remote urls = %v, want %v", returnedRemoteURLs, expectedRemoteURLs) + } + if !seenDisconnected { + return fmt.Errorf("expected at least one disconnected shard sample") + } + return nil + }) + require.NoError(t, err) } diff --git a/sei-tendermint/internal/proxy/proxy.go b/sei-tendermint/internal/proxy/proxy.go index 60d57ea691..8f435fc26f 100644 --- a/sei-tendermint/internal/proxy/proxy.go +++ b/sei-tendermint/internal/proxy/proxy.go @@ -11,6 +11,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" ) // Proxy wraps an ABCI application and records ABCI method timings. @@ -23,9 +24,9 @@ func New(app types.Application) *Proxy { return &Proxy{app: app} } -func (app *Proxy) InitChain(ctx context.Context, req *types.RequestInitChain) (*types.ResponseInitChain, error) { +func (app *Proxy) InitChain(req *types.RequestInitChain) (*types.ResponseInitChain, error) { defer addTimeSample(Global.MethodTimingAt("init_chain", "sync"))() - return app.app.InitChain(ctx, req) + return app.app.InitChain(req) } func (app *Proxy) ProcessProposal(ctx context.Context, req *types.RequestProcessProposal) (*types.ResponseProcessProposal, error) { @@ -80,9 +81,13 @@ func (app *Proxy) CheckTxSafe(ctx context.Context, req *types.RequestCheckTxV2) return res, nil } -func (app *Proxy) Info(ctx context.Context, req *types.RequestInfo) (*types.ResponseInfo, error) { +func (app *Proxy) InitLastHeader(lastHeader *tmproto.Header) { + app.app.InitLastHeader(lastHeader) +} + +func (app *Proxy) Info() *types.ResponseInfo { defer addTimeSample(Global.MethodTimingAt("info", "sync"))() - return app.app.Info(ctx, req) + return app.app.Info() } func (app *Proxy) Query(ctx context.Context, req *types.RequestQuery) (*types.ResponseQuery, error) { diff --git a/sei-tendermint/internal/rpc/core/abci.go b/sei-tendermint/internal/rpc/core/abci.go index a334b0e4c3..ffc068c3ce 100644 --- a/sei-tendermint/internal/rpc/core/abci.go +++ b/sei-tendermint/internal/rpc/core/abci.go @@ -5,7 +5,6 @@ import ( abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" - "github.com/sei-protocol/sei-chain/sei-tendermint/version" ) // ABCIQuery queries the application for some information. @@ -27,10 +26,6 @@ func (env *Environment) ABCIQuery(ctx context.Context, req *coretypes.RequestABC // ABCIInfo gets some info about the application. // More: https://docs.tendermint.com/master/rpc/#/ABCI/abci_info func (env *Environment) ABCIInfo(ctx context.Context) (*coretypes.ResultABCIInfo, error) { - resInfo, err := env.App.Info(ctx, &version.RequestInfo) - if err != nil { - return nil, err - } - + resInfo := env.App.Info() return &coretypes.ResultABCIInfo{Response: *resInfo}, nil } diff --git a/sei-tendermint/internal/rpc/core/blocks.go b/sei-tendermint/internal/rpc/core/blocks.go index 902a9f6863..403fd10ffa 100644 --- a/sei-tendermint/internal/rpc/core/blocks.go +++ b/sei-tendermint/internal/rpc/core/blocks.go @@ -98,7 +98,7 @@ func filterMinMax(base, height, min, max, limit int64) (int64, int64, error) { // individually branching on consensus mode. func (env *Environment) Block(ctx context.Context, req *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { if giga, ok := env.gigaRouter().Get(); ok { - height, err := env.autobahnCheckAndGetHeight(ctx, (*int64)(req.Height)) + height, err := env.autobahnCheckAndGetHeight((*int64)(req.Height)) if err != nil { return nil, err } @@ -144,12 +144,8 @@ func (env *Environment) Block(ctx context.Context, req *coretypes.RequestBlockIn // natural source becomes available once sei-db/ledger_db/block.BlockDB is // wired into block execution: switch this and BlockByNumber to read from // BlockDB, and source `base` from BlockDB.GetLowestBlockHeight. -func (env *Environment) autobahnCheckAndGetHeight(ctx context.Context, heightPtr *int64) (int64, error) { - info, err := env.ABCIInfo(ctx) - if err != nil { - return 0, err - } - return env.getHeight(info.Response.LastBlockHeight, heightPtr) +func (env *Environment) autobahnCheckAndGetHeight(heightPtr *int64) (int64, error) { + return env.getHeight(env.App.Info().LastBlockHeight, heightPtr) } // BlockByHash gets block by hash. @@ -259,7 +255,7 @@ func (env *Environment) Commit(ctx context.Context, req *coretypes.RequestBlockI // populating these under Autobahn is a separate follow-up. func (env *Environment) BlockResults(ctx context.Context, req *coretypes.RequestBlockInfo) (*coretypes.ResultBlockResults, error) { if giga, ok := env.gigaRouter().Get(); ok { - height, err := env.autobahnCheckAndGetHeight(ctx, (*int64)(req.Height)) + height, err := env.autobahnCheckAndGetHeight((*int64)(req.Height)) if err != nil { return nil, err } diff --git a/sei-tendermint/internal/rpc/core/consensus.go b/sei-tendermint/internal/rpc/core/consensus.go index 7e1e5be7a2..45a0e5e416 100644 --- a/sei-tendermint/internal/rpc/core/consensus.go +++ b/sei-tendermint/internal/rpc/core/consensus.go @@ -24,7 +24,7 @@ import ( // mirror the CometBFT path so external tools see consistent responses. func (env *Environment) Validators(ctx context.Context, req *coretypes.RequestValidators) (*coretypes.ResultValidators, error) { if env.gigaRouter().IsPresent() { - height, err := env.autobahnCheckAndGetHeight(ctx, (*int64)(req.Height)) + height, err := env.autobahnCheckAndGetHeight((*int64)(req.Height)) if err != nil { return nil, err } diff --git a/sei-tendermint/internal/state/helpers_test.go b/sei-tendermint/internal/state/helpers_test.go index 033beac048..fbd6da90bd 100644 --- a/sei-tendermint/internal/state/helpers_test.go +++ b/sei-tendermint/internal/state/helpers_test.go @@ -288,8 +288,8 @@ type testApp struct { var _ abci.Application = (*testApp)(nil) -func (app *testApp) Info(_ context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { - return &abci.ResponseInfo{}, nil +func (app *testApp) Info() *abci.ResponseInfo { + return &abci.ResponseInfo{} } func (app *testApp) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { diff --git a/sei-tendermint/internal/statesync/reactor_test.go b/sei-tendermint/internal/statesync/reactor_test.go index c237ae2b9a..9a3e8d08fc 100644 --- a/sei-tendermint/internal/statesync/reactor_test.go +++ b/sei-tendermint/internal/statesync/reactor_test.go @@ -27,7 +27,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/light/provider" pb "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/statesync" "github.com/sei-protocol/sei-chain/sei-tendermint/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/version" ) var m = Global @@ -165,7 +164,7 @@ func TestReactor_Sync(t *testing.T) { appConn.applySnapshotChunk.Set(func(context.Context, *abci.RequestApplySnapshotChunk) (*abci.ResponseApplySnapshotChunk, error) { return &abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}, nil }) - appConn.info.Push(mkHandler(&version.RequestInfo, &abci.ResponseInfo{ + appConn.info.Push(mkConst(&abci.ResponseInfo{ AppVersion: testAppVersion, LastBlockHeight: snapshotHeight, LastBlockAppHash: chain[snapshotHeight+1].AppHash, diff --git a/sei-tendermint/internal/statesync/syncer.go b/sei-tendermint/internal/statesync/syncer.go index b67078b9de..fdda604075 100644 --- a/sei-tendermint/internal/statesync/syncer.go +++ b/sei-tendermint/internal/statesync/syncer.go @@ -15,7 +15,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/light" pb "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/statesync" "github.com/sei-protocol/sei-chain/sei-tendermint/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/version" ) const ( @@ -559,11 +558,7 @@ func (s *syncer) requestChunk(snapshot *snapshot, chunk uint32) { // verifyApp verifies the sync, checking the app hash, last block height and app version func (s *syncer) verifyApp(ctx context.Context, snapshot *snapshot, appVersion uint64) error { - resp, err := s.conn.Info(ctx, &version.RequestInfo) - if err != nil { - return fmt.Errorf("failed to query ABCI app for appHash: %w", err) - } - + resp := s.conn.Info() // sanity check that the app version in the block matches the application's own record // of its version if resp.AppVersion != appVersion { diff --git a/sei-tendermint/internal/statesync/syncer_test.go b/sei-tendermint/internal/statesync/syncer_test.go index ded95bb318..c9b488db09 100644 --- a/sei-tendermint/internal/statesync/syncer_test.go +++ b/sei-tendermint/internal/statesync/syncer_test.go @@ -206,7 +206,7 @@ func TestSyncer_SyncAny(t *testing.T) { &abci.RequestApplySnapshotChunk{Index: 2, Chunk: []byte{1, 1, 2}}, &abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}, )) - app.info.Push(mkHandler(&version.RequestInfo, &abci.ResponseInfo{ + app.info.Push(mkConst(&abci.ResponseInfo{ AppVersion: testAppVersion, LastBlockHeight: 1, LastBlockAppHash: []byte("app_hash"), diff --git a/sei-tendermint/internal/statesync/syncer_verifyapp_default_test.go b/sei-tendermint/internal/statesync/syncer_verifyapp_default_test.go index 44f7384dbe..024b957c99 100644 --- a/sei-tendermint/internal/statesync/syncer_verifyapp_default_test.go +++ b/sei-tendermint/internal/statesync/syncer_verifyapp_default_test.go @@ -5,48 +5,42 @@ package statesync import ( - "context" "errors" "testing" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" - "github.com/sei-protocol/sei-chain/sei-tendermint/version" ) func TestSyncer_verifyApp(t *testing.T) { - boom := errors.New("boom") const appVersion = 9 appVersionMismatchErr := errors.New("app version mismatch. Expected: 9, got: 2") s := &snapshot{Height: 3, Format: 1, Chunks: 5, Hash: []byte{1, 2, 3}, trustedAppHash: []byte("app_hash")} testcases := map[string]struct { response *abci.ResponseInfo - err error expectErr error }{ "verified": {&abci.ResponseInfo{ LastBlockHeight: 3, LastBlockAppHash: []byte("app_hash"), AppVersion: appVersion, - }, nil, nil}, + }, nil}, "invalid app version": {&abci.ResponseInfo{ LastBlockHeight: 3, LastBlockAppHash: []byte("app_hash"), AppVersion: 2, - }, nil, appVersionMismatchErr}, + }, appVersionMismatchErr}, "invalid height": {&abci.ResponseInfo{ LastBlockHeight: 5, LastBlockAppHash: []byte("app_hash"), AppVersion: appVersion, - }, nil, errVerifyFailed}, + }, errVerifyFailed}, "invalid hash": {&abci.ResponseInfo{ LastBlockHeight: 3, LastBlockAppHash: []byte("xxx"), AppVersion: appVersion, - }, nil, errVerifyFailed}, - "error": {nil, boom, boom}, + }, errVerifyFailed}, } for name, tc := range testcases { @@ -56,10 +50,7 @@ func TestSyncer_verifyApp(t *testing.T) { rts := setup(t, nil, nil, true) app := rts.conn - app.info.Push(func(_ context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { - utils.OrPanic(utils.TestDiff(&version.RequestInfo, req)) - return tc.response, tc.err - }) + app.info.Push(mkConst(tc.response)) err := rts.reactor.syncer.verifyApp(ctx, s, appVersion) unwrapped := errors.Unwrap(err) if unwrapped != nil { diff --git a/sei-tendermint/internal/statesync/test_app_test.go b/sei-tendermint/internal/statesync/test_app_test.go index fd88d944ad..05af71d006 100644 --- a/sei-tendermint/internal/statesync/test_app_test.go +++ b/sei-tendermint/internal/statesync/test_app_test.go @@ -2,7 +2,6 @@ package statesync import ( "context" - "log" "sync" "testing" @@ -14,6 +13,10 @@ import ( type Handler[V, R any] = func(context.Context, V) (R, error) +func mkConst[R any](r R) func() R { + return func() R { return r } +} + func mkHandler[V, R any](v V, r R) Handler[V, R] { return func(_ context.Context, got V) (R, error) { utils.OrPanic(utils.TestDiff(v, got)) @@ -21,15 +24,15 @@ func mkHandler[V, R any](v V, r R) Handler[V, R] { } } -type Queue[V, R any] struct { - Handlers []Handler[V, R] - Fallback Handler[V, R] +type Queue[T any] struct { + Handlers []T + Fallback T } -func (q *Queue[V, R]) Len() int { return len(q.Handlers) } -func (q *Queue[V, R]) Set(v Handler[V, R]) { q.Fallback = v } -func (q *Queue[V, R]) Push(v Handler[V, R]) { q.Handlers = append(q.Handlers, v) } -func (q *Queue[V, R]) Pop() Handler[V, R] { +func (q *Queue[T]) Len() int { return len(q.Handlers) } +func (q *Queue[T]) Set(v T) { q.Fallback = v } +func (q *Queue[T]) Push(v T) { q.Handlers = append(q.Handlers, v) } +func (q *Queue[T]) Pop() T { if len(q.Handlers) > 0 { res := q.Handlers[0] q.Handlers = q.Handlers[1:] @@ -42,11 +45,11 @@ type testStatesyncApp struct { *abci.BaseApplication mu sync.Mutex - offerSnapshot Queue[*abci.RequestOfferSnapshot, *abci.ResponseOfferSnapshot] - applySnapshotChunk Queue[*abci.RequestApplySnapshotChunk, *abci.ResponseApplySnapshotChunk] - listSnapshots Queue[*abci.RequestListSnapshots, *abci.ResponseListSnapshots] - loadSnapshotChunk Queue[*abci.RequestLoadSnapshotChunk, *abci.ResponseLoadSnapshotChunk] - info Queue[*abci.RequestInfo, *abci.ResponseInfo] + offerSnapshot Queue[Handler[*abci.RequestOfferSnapshot, *abci.ResponseOfferSnapshot]] + applySnapshotChunk Queue[Handler[*abci.RequestApplySnapshotChunk, *abci.ResponseApplySnapshotChunk]] + listSnapshots Queue[Handler[*abci.RequestListSnapshots, *abci.ResponseListSnapshots]] + loadSnapshotChunk Queue[Handler[*abci.RequestLoadSnapshotChunk, *abci.ResponseLoadSnapshotChunk]] + info Queue[func() *abci.ResponseInfo] } func newTestStatesyncApp() *testStatesyncApp { @@ -68,7 +71,6 @@ func (app *testStatesyncApp) ApplySnapshotChunk(ctx context.Context, req *abci.R } func (app *testStatesyncApp) ListSnapshots(ctx context.Context, req *abci.RequestListSnapshots) (*abci.ResponseListSnapshots, error) { - log.Printf("QQQQQQ ListSnapshots()\n") app.mu.Lock() h := app.listSnapshots.Pop() app.mu.Unlock() @@ -82,11 +84,11 @@ func (app *testStatesyncApp) LoadSnapshotChunk(ctx context.Context, req *abci.Re return h(ctx, req) } -func (app *testStatesyncApp) Info(ctx context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { +func (app *testStatesyncApp) Info() *abci.ResponseInfo { app.mu.Lock() h := app.info.Pop() app.mu.Unlock() - return h(ctx, req) + return h() } func (app *testStatesyncApp) AssertExpectations(t testing.TB) { diff --git a/sei-tendermint/rpc/client/helpers_test.go b/sei-tendermint/rpc/client/helpers_test.go deleted file mode 100644 index f77753d5a8..0000000000 --- a/sei-tendermint/rpc/client/helpers_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package client_test - -import ( - "errors" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" -) - -func TestWaitForHeight(t *testing.T) { - ctx := t.Context() - - // test with error result - immediate failure - m := &mock.StatusMock{ - Call: mock.Call{ - Error: errors.New("bye"), - }, - } - r := mock.NewStatusRecorder(m) - - // connection failure always leads to error - err := client.WaitForHeight(ctx, r, 8, nil) - require.Error(t, err) - require.Equal(t, "bye", err.Error()) - - // we called status once to check - require.Equal(t, 1, len(r.Calls)) - - // now set current block height to 10 - m.Call = mock.Call{ - Response: &coretypes.ResultStatus{SyncInfo: coretypes.SyncInfo{LatestBlockHeight: 10}}, - } - - // we will not wait for more than 10 blocks - err = client.WaitForHeight(ctx, r, 40, nil) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "aborting")) - - // we called status once more to check - require.Equal(t, 2, len(r.Calls)) - - // waiting for the past returns immediately - err = client.WaitForHeight(ctx, r, 5, nil) - require.NoError(t, err) - - // we called status once more to check - require.Equal(t, 3, len(r.Calls)) - - // since we can't update in a background goroutine (test --race) - // we use the callback to update the status height - myWaiter := func(delta int64) error { - // update the height for the next call - m.Call.Response = &coretypes.ResultStatus{SyncInfo: coretypes.SyncInfo{LatestBlockHeight: 15}} - return client.DefaultWaitStrategy(delta) - } - - // we wait for a few blocks - err = client.WaitForHeight(ctx, r, 12, myWaiter) - require.NoError(t, err) - - // we called status once to check - require.Equal(t, 5, len(r.Calls)) - - pre := r.Calls[3] - require.Nil(t, pre.Error) - prer, ok := pre.Response.(*coretypes.ResultStatus) - require.True(t, ok) - assert.Equal(t, int64(10), prer.SyncInfo.LatestBlockHeight) - - post := r.Calls[4] - require.Nil(t, post.Error) - postr, ok := post.Response.(*coretypes.ResultStatus) - require.True(t, ok) - assert.Equal(t, int64(15), postr.SyncInfo.LatestBlockHeight) -} diff --git a/sei-tendermint/rpc/client/mock/client.go b/sei-tendermint/rpc/client/mock/client.go deleted file mode 100644 index 1f6f4249ac..0000000000 --- a/sei-tendermint/rpc/client/mock/client.go +++ /dev/null @@ -1,171 +0,0 @@ -package mock - -/* -package mock returns a Client implementation that -accepts various (mock) implementations of the various methods. - -This implementation is useful for using in tests, when you don't -need a real server, but want a high-level of control about -the server response you want to mock (eg. error handling), -or if you just want to record the calls to verify in your tests. - -For real clients, you probably want the "http" package. If you -want to directly call a tendermint node in process, you can use the -"local" package. -*/ - -import ( - "context" - "reflect" - - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/rpc/core" - "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" - "github.com/sei-protocol/sei-chain/sei-tendermint/types" -) - -// Client wraps arbitrary implementations of the various interfaces. -type Client struct { - client.Client - env *core.Environment -} - -func New() Client { - return Client{ - env: &core.Environment{}, - } -} - -var _ client.Client = Client{} - -// Call is used by recorders to save a call and response. -// It can also be used to configure mock responses. -type Call struct { - Name string - Args interface{} - Response interface{} - Error error -} - -// GetResponse will generate the apporiate response for us, when -// using the Call struct to configure a Mock handler. -// -// When configuring a response, if only one of Response or Error is -// set then that will always be returned. If both are set, then -// we return Response if the Args match the set args, Error otherwise. -func (c Call) GetResponse(args interface{}) (interface{}, error) { - // handle the case with no response - if c.Response == nil { - if c.Error == nil { - panic("Misconfigured call, you must set either Response or Error") - } - return nil, c.Error - } - // response without error - if c.Error == nil { - return c.Response, nil - } - // have both, we must check args.... - if reflect.DeepEqual(args, c.Args) { - return c.Response, nil - } - return nil, c.Error -} - -func (c Client) Status(ctx context.Context) (*coretypes.ResultStatus, error) { - return c.env.Status(ctx) -} - -func (c Client) LagStatus(ctx context.Context) (*coretypes.ResultLagStatus, error) { - return c.env.LagStatus(ctx) -} - -func (c Client) ABCIInfo(ctx context.Context) (*coretypes.ResultABCIInfo, error) { - return c.env.ABCIInfo(ctx) -} - -func (c Client) ABCIQuery(ctx context.Context, path string, data bytes.HexBytes) (*coretypes.ResultABCIQuery, error) { - return c.ABCIQueryWithOptions(ctx, path, data, client.DefaultABCIQueryOptions) -} - -func (c Client) ABCIQueryWithOptions( - ctx context.Context, - path string, - data bytes.HexBytes, - opts client.ABCIQueryOptions) (*coretypes.ResultABCIQuery, error) { - return c.env.ABCIQuery(ctx, &coretypes.RequestABCIQuery{ - Path: path, Data: data, Height: coretypes.Int64(opts.Height), Prove: opts.Prove, - }) -} - -func (c Client) BroadcastTxCommit(ctx context.Context, tx types.Tx) (*coretypes.ResultBroadcastTxCommit, error) { - return c.env.BroadcastTxCommit(ctx, &coretypes.RequestBroadcastTx{Tx: tx}) -} - -func (c Client) BroadcastTxAsync(ctx context.Context, tx types.Tx) (*coretypes.ResultBroadcastTx, error) { - return c.env.BroadcastTxAsync(ctx, &coretypes.RequestBroadcastTx{Tx: tx}) -} - -func (c Client) BroadcastTxSync(ctx context.Context, tx types.Tx) (*coretypes.ResultBroadcastTx, error) { - return c.env.BroadcastTxSync(ctx, &coretypes.RequestBroadcastTx{Tx: tx}) -} - -func (c Client) CheckTx(ctx context.Context, tx types.Tx) (*coretypes.ResultCheckTx, error) { - return c.env.CheckTx(ctx, &coretypes.RequestCheckTx{Tx: tx}) -} - -func (c Client) NetInfo(ctx context.Context) (*coretypes.ResultNetInfo, error) { - return c.env.NetInfo(ctx) -} - -func (c Client) ConsensusState(ctx context.Context) (*coretypes.ResultConsensusState, error) { - return c.env.GetConsensusState(ctx) -} - -func (c Client) DumpConsensusState(ctx context.Context) (*coretypes.ResultDumpConsensusState, error) { - return c.env.DumpConsensusState(ctx) -} - -func (c Client) ConsensusParams(ctx context.Context, height *int64) (*coretypes.ResultConsensusParams, error) { - return c.env.ConsensusParams(ctx, &coretypes.RequestConsensusParams{Height: (*coretypes.Int64)(height)}) -} - -func (c Client) Health(ctx context.Context) (*coretypes.ResultHealth, error) { - return c.env.Health(ctx) -} - -func (c Client) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) { - return c.env.BlockchainInfo(ctx, &coretypes.RequestBlockchainInfo{ - MinHeight: coretypes.Int64(minHeight), - MaxHeight: coretypes.Int64(maxHeight), - }) -} - -func (c Client) Genesis(ctx context.Context) (*coretypes.ResultGenesis, error) { - return c.env.Genesis(ctx) -} - -func (c Client) Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) { - return c.env.Block(ctx, &coretypes.RequestBlockInfo{Height: (*coretypes.Int64)(height)}) -} - -func (c Client) BlockByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultBlock, error) { - return c.env.BlockByHash(ctx, &coretypes.RequestBlockByHash{Hash: hash}) -} - -func (c Client) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { - return c.env.Commit(ctx, &coretypes.RequestBlockInfo{Height: (*coretypes.Int64)(height)}) -} - -func (c Client) Validators(ctx context.Context, height *int64, page, perPage *int) (*coretypes.ResultValidators, error) { - return c.env.Validators(ctx, &coretypes.RequestValidators{ - Height: (*coretypes.Int64)(height), - Page: coretypes.Int64Ptr(page), - PerPage: coretypes.Int64Ptr(perPage), - }) -} - -func (c Client) BroadcastEvidence(ctx context.Context, ev types.Evidence) (*coretypes.ResultBroadcastEvidence, error) { - return c.env.BroadcastEvidence(ctx, &coretypes.RequestBroadcastEvidence{Evidence: ev}) -} diff --git a/sei-tendermint/rpc/client/mock/status.go b/sei-tendermint/rpc/client/mock/status.go deleted file mode 100644 index dbf468803f..0000000000 --- a/sei-tendermint/rpc/client/mock/status.go +++ /dev/null @@ -1,72 +0,0 @@ -package mock - -import ( - "context" - - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" -) - -// StatusMock returns the result specified by the Call -type StatusMock struct { - Call -} - -var ( - _ client.StatusClient = (*StatusMock)(nil) - _ client.StatusClient = (*StatusRecorder)(nil) -) - -func (m *StatusMock) Status(ctx context.Context) (*coretypes.ResultStatus, error) { - res, err := m.GetResponse(nil) - if err != nil { - return nil, err - } - return res.(*coretypes.ResultStatus), nil -} - -func (m *StatusMock) LagStatus(ctx context.Context) (*coretypes.ResultLagStatus, error) { - res, err := m.GetResponse(nil) - if err != nil { - return nil, err - } - return res.(*coretypes.ResultLagStatus), nil -} - -// StatusRecorder can wrap another type (StatusMock, full client) -// and record the status calls -type StatusRecorder struct { - Client client.StatusClient - Calls []Call -} - -func NewStatusRecorder(client client.StatusClient) *StatusRecorder { - return &StatusRecorder{ - Client: client, - Calls: []Call{}, - } -} - -func (r *StatusRecorder) addCall(call Call) { - r.Calls = append(r.Calls, call) -} - -func (r *StatusRecorder) Status(ctx context.Context) (*coretypes.ResultStatus, error) { - res, err := r.Client.Status(ctx) - r.addCall(Call{ - Name: "status", - Response: res, - Error: err, - }) - return res, err -} - -func (r *StatusRecorder) LagStatus(ctx context.Context) (*coretypes.ResultLagStatus, error) { - res, err := r.Client.LagStatus(ctx) - r.addCall(Call{ - Name: "lag_status", - Response: res, - Error: err, - }) - return res, err -} diff --git a/sei-tendermint/rpc/client/mock/status_test.go b/sei-tendermint/rpc/client/mock/status_test.go deleted file mode 100644 index 077bc2c7bf..0000000000 --- a/sei-tendermint/rpc/client/mock/status_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package mock_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/mock" - "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" -) - -func TestStatus(t *testing.T) { - ctx := t.Context() - - m := &mock.StatusMock{ - Call: mock.Call{ - Response: &coretypes.ResultStatus{ - SyncInfo: coretypes.SyncInfo{ - LatestBlockHash: bytes.HexBytes("block"), - LatestAppHash: bytes.HexBytes("app"), - LatestBlockHeight: 10, - MaxPeerBlockHeight: 20, - TotalSyncedTime: time.Second, - RemainingTime: time.Minute, - TotalSnapshots: 10, - ChunkProcessAvgTime: time.Duration(10), - SnapshotHeight: 10, - SnapshotChunksCount: 9, - SnapshotChunksTotal: 10, - BackFilledBlocks: 9, - BackFillBlocksTotal: 10, - }, - }}, - } - - r := mock.NewStatusRecorder(m) - require.Equal(t, 0, len(r.Calls)) - - // make sure response works proper - status, err := r.Status(ctx) - require.NoError(t, err) - assert.EqualValues(t, "block", status.SyncInfo.LatestBlockHash) - assert.EqualValues(t, 10, status.SyncInfo.LatestBlockHeight) - assert.EqualValues(t, 20, status.SyncInfo.MaxPeerBlockHeight) - assert.EqualValues(t, time.Second, status.SyncInfo.TotalSyncedTime) - assert.EqualValues(t, time.Minute, status.SyncInfo.RemainingTime) - - // make sure recorder works properly - require.Equal(t, 1, len(r.Calls)) - rs := r.Calls[0] - assert.Equal(t, "status", rs.Name) - assert.Nil(t, rs.Args) - assert.Nil(t, rs.Error) - require.NotNil(t, rs.Response) - st, ok := rs.Response.(*coretypes.ResultStatus) - require.True(t, ok) - assert.EqualValues(t, "block", st.SyncInfo.LatestBlockHash) - assert.EqualValues(t, 10, st.SyncInfo.LatestBlockHeight) - assert.EqualValues(t, 20, st.SyncInfo.MaxPeerBlockHeight) - assert.EqualValues(t, time.Second, status.SyncInfo.TotalSyncedTime) - assert.EqualValues(t, time.Minute, status.SyncInfo.RemainingTime) - - assert.EqualValues(t, 10, st.SyncInfo.TotalSnapshots) - assert.EqualValues(t, time.Duration(10), st.SyncInfo.ChunkProcessAvgTime) - assert.EqualValues(t, 10, st.SyncInfo.SnapshotHeight) - assert.EqualValues(t, 9, status.SyncInfo.SnapshotChunksCount) - assert.EqualValues(t, 10, status.SyncInfo.SnapshotChunksTotal) - assert.EqualValues(t, 9, status.SyncInfo.BackFilledBlocks) - assert.EqualValues(t, 10, status.SyncInfo.BackFillBlocksTotal) -} diff --git a/sei-tendermint/test/e2e/app/app.go b/sei-tendermint/test/e2e/app/app.go index aa07ef3f6a..bdcc04e4db 100644 --- a/sei-tendermint/test/e2e/app/app.go +++ b/sei-tendermint/test/e2e/app/app.go @@ -110,7 +110,7 @@ func NewApplication(cfg *Config) (*Application, error) { } // Info implements ABCI. -func (app *Application) Info(_ context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { +func (app *Application) Info() *abci.ResponseInfo { app.mu.Lock() defer app.mu.Unlock() @@ -119,11 +119,11 @@ func (app *Application) Info(_ context.Context, req *abci.RequestInfo) (*abci.Re AppVersion: 1, LastBlockHeight: int64(app.state.Height), //nolint:gosec // Height is a non-negative block height LastBlockAppHash: app.state.Hash, - }, nil + } } // Info implements ABCI. -func (app *Application) InitChain(_ context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { +func (app *Application) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { app.mu.Lock() defer app.mu.Unlock() diff --git a/sei-wasmd/app/app_test.go b/sei-wasmd/app/app_test.go index 772aa8879a..17afc01d1d 100644 --- a/sei-wasmd/app/app_test.go +++ b/sei-wasmd/app/app_test.go @@ -25,7 +25,7 @@ func TestWasmdExport(t *testing.T) { require.NoError(t, err) // Initialize the chain - gapp.InitChain(t.Context(), &abci.RequestInitChain{AppStateBytes: stateBytes}) + gapp.InitChain(&abci.RequestInitChain{AppStateBytes: stateBytes}) gapp.SetDeliverStateToCommit() gapp.Commit(context.Background()) diff --git a/sei-wasmd/app/test_helpers.go b/sei-wasmd/app/test_helpers.go index f65bd14fac..e8057d7b49 100644 --- a/sei-wasmd/app/test_helpers.go +++ b/sei-wasmd/app/test_helpers.go @@ -165,13 +165,11 @@ func SetupWithGenesisValSet(t *testing.T, chainID string, valSet *tmtypes.Valida require.NoError(t, err) // init chain will set the validator set and initialize the genesis accounts - _, err = app.InitChain( - context.Background(), - &abci.RequestInitChain{ - ConsensusParams: DefaultConsensusParams, - AppStateBytes: stateBytes, - ChainId: chainID, - }, + _, err = app.InitChain(&abci.RequestInitChain{ + ConsensusParams: DefaultConsensusParams, + AppStateBytes: stateBytes, + ChainId: chainID, + }, ) require.NoError(t, err) // This line is necessary due to the capability module which has a map as well as store usages, diff --git a/sei-wasmd/benchmarks/app_test.go b/sei-wasmd/benchmarks/app_test.go index 5ff9bcaf66..db7d47f4ac 100644 --- a/sei-wasmd/benchmarks/app_test.go +++ b/sei-wasmd/benchmarks/app_test.go @@ -53,12 +53,10 @@ func SetupWithGenesisAccounts(b testing.TB, db dbm.DB, genAccs []authtypes.Genes panic(err) } - wasmApp.InitChain( - context.Background(), - &abci.RequestInitChain{ - ConsensusParams: seiapp.DefaultConsensusParams, - AppStateBytes: stateBytes, - }, + wasmApp.InitChain(&abci.RequestInitChain{ + ConsensusParams: seiapp.DefaultConsensusParams, + AppStateBytes: stateBytes, + }, ) wasmApp.SetDeliverStateToCommit() diff --git a/testutil/evmtest/docker.go b/testutil/evmtest/docker.go new file mode 100644 index 0000000000..933231be0c --- /dev/null +++ b/testutil/evmtest/docker.go @@ -0,0 +1,87 @@ +package evmtest + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" +) + +type DockerTxConfig struct { + Container string + Password string + From string + Recipient string + ChainID string + EVMRPCURL string +} + +func DefaultDockerTxConfig() DockerTxConfig { + return DockerTxConfig{ + Container: "sei-node-0", + Password: "12345678", + From: "admin", + Recipient: "0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52", + ChainID: "sei", + EVMRPCURL: "http://localhost:8545", + } +} + +func ConfigFromEnv(prefix string) DockerTxConfig { + cfg := DefaultDockerTxConfig() + if v := os.Getenv(prefix + "CONTAINER"); v != "" { + cfg.Container = v + } + if v := os.Getenv(prefix + "PASSWORD"); v != "" { + cfg.Password = v + } + if v := os.Getenv(prefix + "FROM"); v != "" { + cfg.From = v + } + if v := os.Getenv(prefix + "RECIPIENT"); v != "" { + cfg.Recipient = v + } + if v := os.Getenv(prefix + "CHAIN_ID"); v != "" { + cfg.ChainID = v + } + if v := os.Getenv(prefix + "EVM_RPC_URL"); v != "" { + cfg.EVMRPCURL = v + } + return cfg +} + +// SendTinyEvmTx submits a dust EVM transfer whose only purpose is to force one +// real block under allow_empty_blocks=false. Callers use it as a liveness/head +// trigger, not to validate transfer semantics. +func SendTinyEvmTx(ctx context.Context, cfg DockerTxConfig) (string, error) { + cmd := exec.CommandContext( //nolint:gosec // test-only helper invokes docker with fixed argv shape + ctx, + "docker", "exec", + "--env", fmt.Sprintf("SEI_EVM_PASSWORD=%s", cfg.Password), + cfg.Container, + "/bin/bash", "-c", + `export PATH=$PATH:/root/go/bin && printf "%s\n" "$SEI_EVM_PASSWORD" | "$@"`, + "bash", + "seid", "tx", "evm", "send", cfg.Recipient, "1", + "--from", cfg.From, + "--chain-id", cfg.ChainID, + "--evm-rpc", cfg.EVMRPCURL, + "-b", "sync", + "-y", + ) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("send tiny evm tx: %w\n%s", err, out) + } + return parseTxHash(string(out)) +} + +func parseTxHash(output string) (string, error) { + for _, line := range strings.Split(output, "\n") { + if hash, ok := strings.CutPrefix(strings.TrimSpace(line), "Transaction hash: "); ok { + return strings.TrimSpace(hash), nil + } + } + return "", fmt.Errorf("transaction hash not found in output:\n%s", output) +} diff --git a/testutil/processblock/common.go b/testutil/processblock/common.go index e767e43f92..2bf1943211 100644 --- a/testutil/processblock/common.go +++ b/testutil/processblock/common.go @@ -48,7 +48,7 @@ func NewTestApp(t *testing.T) *App { if err != nil { panic(err) } - _, err = a.InitChain(context.Background(), &types.RequestInitChain{ + _, err = a.InitChain(&types.RequestInitChain{ Time: time.Now(), ChainId: "tendermint_test", ConsensusParams: &cp, diff --git a/x/evm/ante/sig_test.go b/x/evm/ante/sig_test.go index adda549be0..06c8ff0e65 100644 --- a/x/evm/ante/sig_test.go +++ b/x/evm/ante/sig_test.go @@ -158,3 +158,48 @@ func TestSigVerifyPendingTransaction(t *testing.T) { }) require.NotNil(t, err) } + +func TestEVMSimulationBeforeFirstCommitIsNotGenesis(t *testing.T) { + k := &testkeeper.EVMTestApp.EvmKeeper + ctx := testkeeper.EVMTestApp.GetCheckCtx().WithBlockTime(time.Now()) + require.False(t, ctx.IsGenesis()) + + handler := ante.NewEVMSigVerifyDecorator(k, func() sdk.Context { return ctx }) + privKey := testkeeper.MockPrivateKey() + testPrivHex := hex.EncodeToString(privKey.Bytes()) + key, _ := crypto.HexToECDSA(testPrivHex) + to := new(common.Address) + copy(to[:], []byte("0x1234567890abcdef1234567890abcdef12345678")) + txData := ethtypes.LegacyTx{ + Nonce: 0, + GasPrice: big.NewInt(10), + Gas: 1000, + To: to, + Value: big.NewInt(1000), + Data: []byte("abc"), + } + chainID := k.ChainID(ctx) + chainCfg := types.DefaultChainConfig() + ethCfg := chainCfg.EthereumConfig(chainID) + blockNum := big.NewInt(ctx.BlockHeight()) + signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) + tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) + require.NoError(t, err) + typedTx, err := ethtx.NewLegacyTx(tx) + require.NoError(t, err) + msg, err := types.NewMsgEVMTransaction(typedTx) + require.NoError(t, err) + + preprocessor := ante.NewEVMPreprocessDecorator(k, k.AccountKeeper()) + ctx, err = preprocessor.AnteHandle(ctx, mockTx{msgs: []sdk.Msg{msg}}, true, func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + require.False(t, ctx.IsGenesis()) + return ctx, nil + }) + require.NoError(t, err) + + _, err = handler.AnteHandle(ctx, mockTx{msgs: []sdk.Msg{msg}}, true, func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + require.False(t, ctx.IsGenesis()) + return ctx, nil + }) + require.NoError(t, err) +}