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
60 changes: 51 additions & 9 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 @@ -695,6 +698,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 @@ -1667,6 +1672,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 @@ -1679,6 +1690,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 @@ -1837,6 +1854,21 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
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.
baseFee := app.GigaEvmKeeper.GetBaseFee(ctx)
if baseFee == nil {
baseFee = new(big.Int) // default to 0 when base fee is unset
}
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 @@ -1856,12 +1888,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 @@ -1871,8 +1916,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 @@ -1908,9 +1953,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
129 changes: 129 additions & 0 deletions contracts/src/MultiHopSwapTester.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
* @title SimpleToken
* @dev Minimal ERC20 token for testing multi-hop swaps.
* Deployer gets an initial mint and can mint more for testing.
*/
contract SimpleToken is ERC20 {
constructor(string memory name_, string memory symbol_, uint256 initialSupply) ERC20(name_, symbol_) {
_mint(msg.sender, initialSupply);
}

function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}

/**
* @title SimplePair
* @dev Minimal Uniswap V2-style constant-product AMM pair.
* Holds reserves of two tokens and allows swaps between them.
* No LP tokens, fees, or flash-loan protection — just the swap math
* needed to exercise multi-hop cross-contract token transfers.
*/
contract SimplePair {
address public token0;
address public token1;
uint256 public reserve0;
uint256 public reserve1;

event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to);
event Sync(uint256 reserve0, uint256 reserve1);

constructor(address _token0, address _token1) {
token0 = _token0;
token1 = _token1;
}

/**
* @dev Add initial liquidity. Tokens must already be transferred to this contract.
*/
function addLiquidity() external {
reserve0 = ERC20(token0).balanceOf(address(this));
reserve1 = ERC20(token1).balanceOf(address(this));
emit Sync(reserve0, reserve1);
}

/**
* @dev Swap: caller sends tokenIn to this contract first, then calls swap.
* Calculates output using x*y=k (no fee for simplicity).
* @param amountIn Amount of input token already transferred to this contract
* @param tokenIn Address of the input token
* @param to Recipient of the output tokens
* @return amountOut Amount of output tokens sent
*/
function swap(uint256 amountIn, address tokenIn, address to) external returns (uint256 amountOut) {
require(tokenIn == token0 || tokenIn == token1, "invalid token");
require(amountIn > 0, "zero input");

bool isToken0 = (tokenIn == token0);
(uint256 resIn, uint256 resOut) = isToken0 ? (reserve0, reserve1) : (reserve1, reserve0);
address tokenOut = isToken0 ? token1 : token0;

// x * y = k, so amountOut = resOut - k / (resIn + amountIn)
// With 0.3% fee: amountInWithFee = amountIn * 997
uint256 amountInWithFee = amountIn * 997;
amountOut = (amountInWithFee * resOut) / (resIn * 1000 + amountInWithFee);
require(amountOut > 0, "insufficient output");

// Transfer output to recipient
ERC20(tokenOut).transfer(to, amountOut);

// Update reserves
reserve0 = ERC20(token0).balanceOf(address(this));
reserve1 = ERC20(token1).balanceOf(address(this));

if (isToken0) {
emit Swap(msg.sender, amountIn, 0, 0, amountOut, to);
} else {
emit Swap(msg.sender, 0, amountIn, amountOut, 0, to);
}
emit Sync(reserve0, reserve1);
}
}

/**
* @title SimpleRouter
* @dev Minimal multi-hop swap router. Chains swaps through multiple pairs,
* transferring tokens between contracts at each hop — exactly like
* the Dragonswap router call that triggered the AppHash divergence.
*
* The key property this exercises: many cross-contract ERC20 transferFrom/
* transfer calls within a single top-level tx, each going through
* separate EVM CALL frames with separate EVM snapshots.
*/
contract SimpleRouter {
/**
* @dev Execute a multi-hop swap through a series of pairs.
* @param amountIn Amount of the first token to swap
* @param path Array of token addresses representing the swap path
* e.g. [tokenA, tokenB, tokenC] swaps A→B then B→C
* @param pairs Array of pair addresses for each hop
* e.g. [pairAB, pairBC]
* @param to Final recipient of the output tokens
* @return amountOut Final output amount
*/
function swapExactTokensForTokens(
uint256 amountIn,
address[] calldata path,
address[] calldata pairs,
address to
) external returns (uint256 amountOut) {
require(path.length >= 2, "path too short");
require(pairs.length == path.length - 1, "pairs/path mismatch");

// Pull input tokens from sender to the first pair
ERC20(path[0]).transferFrom(msg.sender, pairs[0], amountIn);

// Chain swaps through each pair
amountOut = amountIn;
for (uint256 i = 0; i < pairs.length; i++) {
address recipient = (i < pairs.length - 1) ? pairs[i + 1] : to;
amountOut = SimplePair(pairs[i]).swap(amountOut, path[i], recipient);
}
}
}
Loading
Loading