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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ jobs:
"./integration_test/evm_module/scripts/evm_giga_tests.sh"
]
},
{
name: "EVM GIGA Mixed (Determinism)",
scripts: [
"./integration_test/evm_module/scripts/evm_giga_mixed_tests.sh"
]
},
]
steps:
- uses: actions/checkout@v3
Expand Down
50 changes: 50 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,56 @@ giga-integration-test:
@echo "=== GIGA Integration Tests Complete ==="
.PHONY: giga-integration-test

# Run a mixed-mode cluster: node 0 uses GIGA_EXECUTOR, nodes 1-3 use standard V2.
# Any determinism divergence between giga and V2 will cause the giga node to halt.
docker-cluster-start-giga-mixed: docker-cluster-stop build-docker-node
@rm -rf $(PROJECT_HOME)/build/generated
@mkdir -p $(shell go env GOPATH)/pkg/mod
@mkdir -p $(shell go env GOCACHE)
@cd docker && \
if [ "$${DOCKER_DETACH:-}" = "true" ]; then \
DETACH_FLAG="-d"; \
else \
DETACH_FLAG=""; \
fi; \
DOCKER_PLATFORM=$(DOCKER_PLATFORM) USERID=$(shell id -u) GROUPID=$(shell id -g) GOCACHE=$(shell go env GOCACHE) NUM_ACCOUNTS=10 INVARIANT_CHECK_INTERVAL=${INVARIANT_CHECK_INTERVAL} UPGRADE_VERSION_LIST=${UPGRADE_VERSION_LIST} MOCK_BALANCES=${MOCK_BALANCES} \
docker compose -f docker-compose.yml -f docker-compose.giga-mixed.yml up $$DETACH_FLAG
.PHONY: docker-cluster-start-giga-mixed

# Run the giga mixed-mode integration test.
# Starts a cluster where only node 0 runs giga (sequential), nodes 1-3 run standard V2.
# Then runs hardhat tests. If giga produces different results, node 0 will halt.
giga-mixed-integration-test:
@echo "=== Starting GIGA Mixed-Mode Integration Tests ==="
@echo "=== Node 0: GIGA_EXECUTOR=true, Nodes 1-3: standard V2 ==="
@$(MAKE) docker-cluster-stop || true
@rm -rf $(PROJECT_HOME)/build/generated
@DOCKER_DETACH=true $(MAKE) docker-cluster-start-giga-mixed
@echo "Waiting for cluster to be ready..."
@timeout=300; elapsed=0; \
while [ $$elapsed -lt $$timeout ]; do \
if [ -f "build/generated/launch.complete" ] && [ $$(cat build/generated/launch.complete | wc -l) -ge 4 ]; then \
echo "All 4 nodes are ready (took $${elapsed}s)"; \
break; \
fi; \
sleep 5; \
elapsed=$$((elapsed + 5)); \
echo " Waiting... ($${elapsed}s elapsed)"; \
done; \
if [ $$elapsed -ge $$timeout ]; then \
echo "ERROR: Cluster failed to start within $${timeout}s"; \
$(MAKE) docker-cluster-stop; \
exit 1; \
fi
@echo "Waiting 10s for nodes to stabilize..."
@sleep 10
@echo "=== Running GIGA EVM Tests (mixed mode) ==="
@./integration_test/evm_module/scripts/evm_giga_tests.sh || (echo "TEST FAILURE - check if node 0 (giga) halted due to consensus mismatch" && $(MAKE) docker-cluster-stop && exit 1)
@echo "=== Stopping cluster ==="
@$(MAKE) docker-cluster-stop
@echo "=== GIGA Mixed-Mode Integration Tests Complete ==="
.PHONY: giga-mixed-integration-test

# Implements test splitting and running. This is pulled directly from
# the github action workflows for better local reproducibility.

Expand Down
130 changes: 120 additions & 10 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"math"
"math/big"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -89,10 +90,12 @@ import (
upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/tracing"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/ethclient"
ethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/holiman/uint256"
"github.com/sei-protocol/sei-chain/giga/deps/tasks"

"github.com/gogo/protobuf/proto"
Expand Down Expand Up @@ -174,6 +177,7 @@ import (
gigabankkeeper "github.com/sei-protocol/sei-chain/giga/deps/xbank/keeper"
gigaevmkeeper "github.com/sei-protocol/sei-chain/giga/deps/xevm/keeper"
gigaevmstate "github.com/sei-protocol/sei-chain/giga/deps/xevm/state"
"github.com/sei-protocol/sei-chain/giga/deps/xevm/types/ethtx"
)

// this line is used by starport scaffolding # stargate/wasm/app/enabledProposals
Expand Down Expand Up @@ -697,6 +701,8 @@ func New(
tkeys[evmtypes.TransientStoreKey], app.GetSubspace(evmtypes.ModuleName), app.receiptStore, app.GigaBankKeeper,
&app.AccountKeeper, &app.StakingKeeper, app.TransferKeeper,
wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper), &app.WasmKeeper, &app.UpgradeKeeper)
app.GigaEvmKeeper.UseRegularStore = true
app.GigaBankKeeper.UseRegularStore = true
app.GigaBankKeeper.RegisterRecipientChecker(app.GigaEvmKeeper.CanAddressReceive)
// Read Giga Executor config
gigaExecutorConfig, err := gigaconfig.ReadConfig(appOpts)
Expand Down Expand Up @@ -1678,6 +1684,12 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req BlockProcessRequ
evmTxs[originalIndex] = app.GetEVMMsg(prioritizedTypedTxs[relativePrioritizedIndex])
}

// Flush giga stores so WriteDeferredBalances (which uses the standard BankKeeper)
// can see balance changes made by the giga executor via GigaBankKeeper.
if app.GigaExecutorEnabled {
ctx.GigaMultiStore().WriteGiga()
}

// Finalize all Bank Module Transfers here so that events are included for prioritiezd txs
deferredWriteEvents := app.BankKeeper.WriteDeferredBalances(ctx)
events = append(events, deferredWriteEvents...)
Expand All @@ -1690,6 +1702,12 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req BlockProcessRequ
txResults[originalIndex] = otherResults[relativeOtherIndex]
evmTxs[originalIndex] = app.GetEVMMsg(otherTypedTxs[relativeOtherIndex])
}

// Flush giga stores after second round (same reason as above)
if app.GigaExecutorEnabled {
ctx.GigaMultiStore().WriteGiga()
}

app.EvmKeeper.SetTxResults(txResults)
app.EvmKeeper.SetMsgs(evmTxs)

Expand Down Expand Up @@ -1742,13 +1760,95 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
}
}

// ============================================================================
// Fee validation (mirrors V2's ante handler checks in evm_checktx.go)
// NOTE: In V2, failed transactions still increment nonce and charge gas.
// We track validation errors here but don't return early - we still need to
// create stateDB, increment nonce, and finalize state to match V2 behavior.
// ============================================================================
baseFee := app.GigaEvmKeeper.GetBaseFee(ctx)
if baseFee == nil {
baseFee = new(big.Int) // default to 0 when base fee is unset
}

// Track validation errors - we'll skip execution but still finalize state
var validationErr *abci.ExecTxResult

// 1. Fee cap < base fee check (INSUFFICIENT_MAX_FEE_PER_GAS)
// V2: evm_checktx.go line 284-286
if txData.GetGasFeeCap().Cmp(baseFee) < 0 {
validationErr = &abci.ExecTxResult{
Code: sdkerrors.ErrInsufficientFee.ABCICode(),
Log: "max fee per gas less than block base fee",
}
}

// 2. Tip > fee cap check (PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS)
// This is checked in txData.Validate() for DynamicFeeTx, but we also check here
// to ensure consistent rejection before execution.
if validationErr == nil && txData.GetGasTipCap().Cmp(txData.GetGasFeeCap()) > 0 {
validationErr = &abci.ExecTxResult{
Code: 1,
Log: "max priority fee per gas higher than max fee per gas",
}
}

// 3. Gas limit * gas price overflow check (GASLIMIT_PRICE_PRODUCT_OVERFLOW)
// V2: Uses IsValidInt256(tx.Fee()) in dynamic_fee_tx.go Validate()
// Fee = GasFeeCap * GasLimit, must fit in 256 bits
if validationErr == nil && !ethtx.IsValidInt256(txData.Fee()) {
validationErr = &abci.ExecTxResult{
Code: 1,
Log: "fee out of bound",
}
}

// 4. TX gas limit > block gas limit check (GAS_ALLOWANCE_EXCEEDED)
// V2: x/evm/ante/basic.go lines 63-68
if validationErr == nil {
if cp := ctx.ConsensusParams(); cp != nil && cp.Block != nil {
if cp.Block.MaxGas > 0 && ethTx.Gas() > uint64(cp.Block.MaxGas) { //nolint:gosec
validationErr = &abci.ExecTxResult{
Code: sdkerrors.ErrOutOfGas.ABCICode(),
Log: fmt.Sprintf("tx gas limit %d exceeds block max gas %d", ethTx.Gas(), cp.Block.MaxGas),
}
}
}
}

// Prepare context for EVM transaction (set infinite gas meter like original flow)
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx))

// Create state DB for this transaction
// If validation failed, increment nonce via keeper (matching V2's DeliverTxCallback behavior
// in x/evm/ante/basic.go). V2 does NOT create stateDB or handle surplus for early failures.
if validationErr != nil {
// Match V2 error handling: bump nonce directly via keeper (not stateDB)
currentNonce := app.GigaEvmKeeper.GetNonce(ctx, sender)
app.GigaEvmKeeper.SetNonce(ctx, sender, currentNonce+1)

// V2 reports intrinsic gas as gasUsed even on validation failure (for metrics),
// but no actual balance is deducted
intrinsicGas, _ := core.IntrinsicGas(ethTx.Data(), ethTx.AccessList(), ethTx.SetCodeAuthorizations(), ethTx.To() == nil, true, true, true)
validationErr.GasUsed = int64(intrinsicGas) //nolint:gosec
validationErr.GasWanted = int64(ethTx.Gas()) //nolint:gosec
return validationErr, nil
}

// Create state DB for this transaction (only for valid transactions)
stateDB := gigaevmstate.NewDBImpl(ctx, &app.GigaEvmKeeper, false)
defer stateDB.Cleanup()

// Pre-charge gas fee (like V2's ante handler), then execute with feeAlreadyCharged=true.
// V2 charges fees in the ante handler, then runs the EVM with feeAlreadyCharged=true
// which skips buyGas/refundGas/coinbase. Without this, GasUsed differs between Giga
// and V2, causing LastResultsHash → AppHash divergence.
effectiveGasPrice := new(big.Int).Add(new(big.Int).Set(ethTx.GasTipCap()), baseFee)
if effectiveGasPrice.Cmp(ethTx.GasFeeCap()) > 0 {
effectiveGasPrice.Set(ethTx.GasFeeCap())
}
gasFee := new(big.Int).Mul(new(big.Int).SetUint64(ethTx.Gas()), effectiveGasPrice)
stateDB.SubBalance(sender, uint256.MustFromBig(gasFee), tracing.BalanceDecreaseGasBuy)

// Get gas pool
gp := app.GigaEvmKeeper.GetGasPool()

Expand All @@ -1768,12 +1868,25 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
// Create Giga executor VM
gigaExecutor := gigaexecutor.NewGethExecutor(*blockCtx, stateDB, cfg, vm.Config{}, gigaprecompiles.AllCustomPrecompilesFailFast)

// Execute the transaction through giga VM
execResult, execErr := gigaExecutor.ExecuteTransaction(ethTx, sender, app.GigaEvmKeeper.GetBaseFee(ctx), &gp)
// Execute with feeAlreadyCharged=true — matching V2's msg_server behavior
execResult, execErr := gigaExecutor.ExecuteTransactionFeeCharged(ethTx, sender, baseFee, &gp)
if execErr != nil {
// Match V2 error handling: bump nonce, commit fee deduction, track surplus
stateDB.SetNonce(sender, stateDB.GetNonce(sender)+1, tracing.NonceChangeEoACall)
surplus, ferr := stateDB.Finalize()
if ferr != nil {
ctx.Logger().Error("giga: failed to finalize stateDB on consensus error",
"txHash", ethTx.Hash().Hex(),
"error", ferr,
)
}
bloom := ethtypes.Bloom{}
app.EvmKeeper.AppendToEvmTxDeferredInfo(ctx, bloom, ethTx.Hash(), surplus)

return &abci.ExecTxResult{
Code: 1,
Log: fmt.Sprintf("giga executor apply message error: %v", execErr),
Code: 1,
GasWanted: int64(ethTx.Gas()), //nolint:gosec
Log: fmt.Sprintf("giga executor apply message error: %v", execErr),
}, nil
}

Expand All @@ -1783,8 +1896,8 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
return nil, execResult.Err
}

// Finalize state changes
_, ferr := stateDB.Finalize()
// Finalize state changes — captures surplus (fee deduction + execution balance changes)
surplus, ferr := stateDB.Finalize()
if ferr != nil {
return &abci.ExecTxResult{
Code: 1,
Expand Down Expand Up @@ -1820,9 +1933,6 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
}

// Append deferred info for EndBlock processing
// Calculate surplus (gas fee paid minus gas used * effective gas price)
// For giga executor, we set surplus to zero since we're not charging gas fees through the normal flow
surplus := sdk.ZeroInt()
bloom := ethtypes.Bloom{}
bloom.SetBytes(receipt.LogsBloom)
app.EvmKeeper.AppendToEvmTxDeferredInfo(ctx, bloom, ethTx.Hash(), surplus)
Expand Down
Loading
Loading