Repository E2E Test Surface

August 26, 2026 ยท View on GitHub

This repository-level surface hosts one black-box acceptance package. Its tests drive IBC CLI through its public CLI, config, readiness, relay, and status contracts, and relay real IBC packets through attestation light clients; the quorum test additionally exercises 2-of-3 quorum loss and recovery.

internal/harness/environment realizes Chains and protocol resources, including the IBC contract stack, attestation light clients, and attestor processes. internal/e2etest deploys a test ERC20, a Counter target, and an IFT token per Chain and binds ICS20 transfers, ICS27 GMP calls, and IFT transfers to routes. Tests deploy the applications and start the relayer explicitly, so process restarts, manual relay, fault injection, and teardown remain visible in the behavior under test.

Acceptance coverage

The root package covers ICS20 transfer, ICS27 GMP, IFT (burn/mint on top of GMP) relay behavior, timeout refunds, error acknowledgements, pending-packet status, Relayer and node recovery, attestor quorum loss and recovery, cross-route handling, and relaying through an attached RPC that Environment does not own. These are all acceptance criteria and run together by default.

Running the acceptance tests

Run targets from the repository root:

make -C e2e doctor
make -C e2e test

The test target builds cli/bin/ibc; IBC_BIN overrides that path. The real relayer collects attestor signatures and submits recv, ack, and timeout transactions with attestation proofs, which the attestation light clients verify.

Execution modes choose providers from each test's declared requirements:

ModeProvider policyUnresolved requirement
fast (default)Prefer AnvilSkip
completePrefer AnvilFail
productionPrefer Besu, then AnvilFail

Portable EVM tests therefore use Anvil in fast and complete modes and Besu in production mode. Tests requiring controlled mining or node lifecycle use Anvil in every mode because Besu does not provide those harness controls. complete runs each test once with the fastest compatible provider; it does not run every provider permutation.

make -C e2e test
make -C e2e test E2E_MODE=complete
make -C e2e test E2E_MODE=production
make -C e2e test E2E_FLAGS='-run TestIFTTransfer_AutoRelay -count=1'
make -C e2e test E2E_MODE=production E2E_FLAGS='-run TestCrossRoute -parallel 1 -count=1'

-e2e.mode in E2E_FLAGS overrides E2E_MODE. After a hard crash, use make -C e2e clean-dry-run and then make -C e2e clean.

Every environment-backed test calls t.Parallel() and boots its own environment; the Makefile caps concurrency at four environments. Pass E2E_FLAGS='-parallel 1 -count=1' to serialize when debugging.

Writing a test

The setup sequence is deliberately explicit:

func TestTransfer_AutoRelay(t *testing.T) {
    t.Parallel()
    spec, runtime := attestedMesh(e2etest.EVMChains(t, e2etest.EVMRequirements{}, e2etest.ChainA, e2etest.ChainB))
    env := e2etest.Start(t, spec, runtime)
    sender := e2etest.NewSigner(t)
    relayerSigner := e2etest.NewSigner(t)
    route := e2etest.AtoB(e2etest.ChainA, e2etest.ChainB)
    driver, deployment := e2etest.Deploy(t, env, sender, relayerSigner, route)
    transferApp := e2etest.NewTransfer(t, env, deployment, sender, route)
    relayer := e2etest.StartRelayer(t, driver, env)
    ctx := t.Context()

    transfer, err := transferApp.Send(ctx, e2etest.TransferRequest{Amount: big.NewInt(1_234_000)})
    require.NoError(t, err)
    require.NoError(t, transfer.VerifyEscrowed(ctx))

    _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(),
        relayerv2.PacketState_PACKET_STATE_SUCCEEDED)
    require.NoError(t, err)
    require.NoError(t, transfer.VerifyDelivered(ctx))
}

Environment owns Chain clients and protocol resources. A route-scoped e2etest application hides only application ABI, transaction, event, and state mechanics. The test keeps deployment, relayer status, fault injection, manual relay, and application assertions visibly ordered; there is no second aggregate beside the Environment.

e2etest.NewSigner creates an independent identity; tests create one per role and pass them explicitly โ€” Deploy takes the deployer and relayer signers, the app constructors take the sender (the signer that deployed the apps). Managed Chains fund them through their resolved funding capability; an attached Chain must fund the public addresses out of band before Deploy. Credentials are written only to protected temporary signer files referenced by alias in the temporary IBC CLI configuration.

Declare capabilities instead of naming a provider. For example, a controlled-mining test uses:

chains := e2etest.EVMChains(t, e2etest.EVMRequirements{ControlledMining: true},
    e2etest.ChainA, e2etest.ChainB)
spec, runtime := attestedMesh(chains)
env := e2etest.Start(t, spec, runtime)

chainB, err := env.Chain(e2etest.ChainB)
require.NoError(t, err)
mining, err := chainB.Mining()
require.NoError(t, err)

An invalid mode or provider fails. Fast mode skips when no compatible provider exists; complete and production modes fail. Startup failures always fail.

Mining and ownership

Managed Anvil starts with one-second mixed mining: transactions are included immediately and idle blocks continue to advance finality. Pausing stops all block production. After resume, Anvil is interval-only, so a transaction may wait up to one second for inclusion.

Environment owns and cleans up only managed resources. An attached EVM remains caller-owned even when the harness can connect to it, and connectivity does not grant mining or node-lifecycle control.

Provider and topology matrix

test-matrix.md is generated from real requirement resolution and environment specs for all three modes. Generation starts the caller-owned Anvil used by the attached-chain test, so Docker is required.

make -C e2e generate-matrix
make -C e2e check-matrix

Regenerate the matrix after changing test requirements or topology. The check compares generated output without modifying the committed file.

Extending the graph

attestedMesh (fixtures_test.go) builds a fully connected attested mesh over the given chains. For sparse graphs or custom attestor topologies (see TestIFTTransfer_MultiAttestorQuorum), write the environment.Spec and matching environment.Runtime literals yourself, with every referenced endpoint and authority. Use e2etest.RuntimeWithProtocolDeployer only when the spec references e2etest.ProtocolAuthorityID, then pass both to e2etest.Start. Application deployment and temporary relay policy stay in the test setup that uses them. The test ERC20 and Counter sources live in internal/harness/environment/solidityibc/contracts, alongside the pinned solidity-ibc-eureka contracts compiled for the harness bindings.