diff --git a/e2e/cmd/custom-ibc/main.go b/e2e/cmd/custom-ibc/main.go new file mode 100644 index 000000000..81c2f477c --- /dev/null +++ b/e2e/cmd/custom-ibc/main.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + + "github.com/cosmos/ibc/link/cli" + "github.com/cosmos/ibc/link/lightclient" + "github.com/cosmos/ibc/link/lightclient/remotepoc" +) + +func main() { + registry := lightclient.NewRegistry() + if err := registry.Register(remotepoc.Factory{}); err != nil { + panic(err) + } + + root := cli.NewRootCmd(cli.Options{ + Relayer: cli.RelayerOptions{ProverFactories: registry}, + }) + os.Exit(cli.Execute(root)) +} diff --git a/e2e/custom_light_client_cli_test.go b/e2e/custom_light_client_cli_test.go new file mode 100644 index 000000000..0ef58985a --- /dev/null +++ b/e2e/custom_light_client_cli_test.go @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 + +package e2e_test + +import ( + "context" + "math/big" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc/e2e/internal/e2etest" + "github.com/cosmos/ibc/e2e/internal/harness/environment" + "github.com/cosmos/ibc/e2e/internal/harness/ibclink" + relayerv2 "github.com/cosmos/ibc/link/api/v2/relayer" + "github.com/cosmos/ibc/link/lightclient/remotepoc" +) + +// TestRemoteAttestationLightClientRelaysPacket relays through a remote prover. +func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { + // Build the downstream CLI that registers the remote prover factory. + t.Setenv("IBC_BIN", buildCustomIBC(t)) + ctx := t.Context() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + spec, runtime := attestedMesh(e2etest.EVMChains( + t, e2etest.EVMRequirements{}, e2etest.ChainA, e2etest.ChainB, + )) + env := e2etest.Start(t, spec, runtime) + sender := e2etest.NewSigner(t) + route := e2etest.ManualAtoB(e2etest.ChainA, e2etest.ChainB) + var serviceConfig ibclink.RelayerConfig + // Keep the attestation config for the proof service, but configure the + // relayer to obtain those proofs from that service over HTTP. + driver, deployment := e2etest.DeployWithRelayerConfig( + t, + env, + sender, + e2etest.NewSigner(t), + func(cfg *ibclink.RelayerConfig) { + require.NotEmpty(t, cfg.Connections) + serviceConfig = cloneRelayerConfig(*cfg) + cfg.Connections[0].ClientAType = remotepoc.Type + cfg.Connections[0].ClientAParams = map[string]any{"url": "http://" + listener.Addr().String()} + }, + route, + ) + // Serve the built-in attestation prover behind the remote prover protocol. + serveAttestationProver(t, listener, env, serviceConfig) + + // Relay a real packet through the custom-compiled CLI and remote prover. + relayer := e2etest.StartRelayer(t, driver, env) + transfer, err := e2etest.NewTransfer(t, env, deployment, sender, route).Send( + ctx, e2etest.TransferRequest{Amount: big.NewInt(1_234_000)}, + ) + require.NoError(t, err) + require.NoError(t, e2etest.RelayAll(ctx, relayer, transfer.PacketTx())) + _, err = e2etest.AwaitState( + ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED, + ) + require.NoError(t, err) + require.NoError(t, transfer.VerifyDelivered(ctx)) +} + +func serveAttestationProver( + t *testing.T, + listener net.Listener, + env *environment.Environment, + cfg ibclink.RelayerConfig, +) { + t.Helper() + useEnvironmentRPCs(t, env, &cfg) + configPath := filepath.Join(t.TempDir(), "attestation-proof-service.yaml") + require.NoError(t, ibclink.WriteRelayerConfig(configPath, cfg)) + + client := cfg.Connections[0] + server, err := remotepoc.NewAttestationHandler(t.Context(), configPath, client.ChainA, client.ClientA) + require.NoError(t, err) + errs := make(chan error, 1) + go func() { errs <- server.Serve(listener) }() + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, server.Shutdown(ctx)) + require.ErrorIs(t, <-errs, http.ErrServerClosed) + }) +} + +func cloneRelayerConfig(cfg ibclink.RelayerConfig) ibclink.RelayerConfig { + cfg.Chains = append([]ibclink.RelayerChain(nil), cfg.Chains...) + cfg.Connections = append([]ibclink.RelayerConnection(nil), cfg.Connections...) + cfg.Attestors = append([]ibclink.RelayerAttestor(nil), cfg.Attestors...) + return cfg +} + +func useEnvironmentRPCs( + t *testing.T, env *environment.Environment, cfg *ibclink.RelayerConfig, +) { + t.Helper() + for i := range cfg.Chains { + for _, id := range env.Chains() { + chain, err := env.Chain(id) + require.NoError(t, err) + if strconv.FormatUint(chain.EVMChainID(), 10) == cfg.Chains[i].ChainID { + cfg.Chains[i].RPC = chain.RPCURL() + break + } + } + require.NotContains(t, cfg.Chains[i].RPC, "${") + } +} + +func buildCustomIBC(t *testing.T) string { + t.Helper() + + binary := filepath.Join(t.TempDir(), "ibc") + cmd := exec.CommandContext(t.Context(), "go", "build", "-o", binary, "./cmd/custom-ibc") + cmd.Env = append(os.Environ(), "GOCACHE="+filepath.Join(t.TempDir(), "go-cache")) + output, err := cmd.CombinedOutput() + require.NoError(t, err, "build custom ibc binary:\n%s", output) + + return binary +} diff --git a/e2e/go.mod b/e2e/go.mod index 3c65165aa..11433fe5c 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -17,6 +17,7 @@ require ( ) require ( + connectrpc.com/grpcreflect v1.3.0 // indirect cosmossdk.io/api v1.0.0 // indirect cosmossdk.io/collections v1.4.0 // indirect cosmossdk.io/core v1.1.0 // indirect @@ -41,6 +42,7 @@ require ( github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b // indirect github.com/cockroachdb/errors v1.13.0 // indirect @@ -69,6 +71,7 @@ require ( github.com/danieljoos/wincred v1.2.3 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/dchest/siphash v1.2.3 // indirect + github.com/deliveryhero/pipeline/v2 v2.2.0 // indirect github.com/desertbit/timer v1.0.1 // indirect github.com/dgraph-io/badger/v4 v4.9.1 // indirect github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect @@ -82,10 +85,12 @@ require ( github.com/fjl/jsonw v0.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/getsentry/sentry-go v0.46.0 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -122,6 +127,10 @@ require ( github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/go-cid v0.5.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect @@ -164,6 +173,7 @@ require ( github.com/multiformats/go-multistream v0.6.1 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729 // indirect github.com/oklog/run v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect @@ -198,9 +208,11 @@ require ( github.com/quic-go/quic-go v0.60.0 // indirect github.com/quic-go/webtransport-go v0.11.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/zerolog v1.35.0 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sasha-s/go-deadlock v0.3.9 // indirect @@ -211,6 +223,7 @@ require ( github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect @@ -271,6 +284,10 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools/v3 v3.5.2 // indirect lukechampine.com/blake3 v1.4.1 // indirect + modernc.org/libc v1.73.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.53.0 // indirect nhooyr.io/websocket v1.8.17 // indirect pgregory.net/rapid v1.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect @@ -347,3 +364,5 @@ require ( ) replace github.com/cosmos/ibc/link => ../link + +replace github.com/cometbft/cometbft => github.com/cometbft/cometbft v0.39.0-rc1.0.20260615134937-9ea34470f336 diff --git a/e2e/go.sum b/e2e/go.sum index f0410e926..6abf34535 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -19,6 +19,8 @@ cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYX cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= +connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= cosmossdk.io/api v1.0.0 h1:qTV8OPVEwcBPwp2b9p4Qy4noZyihJ+sOMaWL/VT+RCc= cosmossdk.io/api v1.0.0/go.mod h1:fKRljeYk+04p4T8Shdyv+uH2fSVyzHoWrvDAs/7OxfI= cosmossdk.io/collections v1.4.0 h1:b373bkxCxKiRbapxZ42TRmcKJEnBVBebdQVk9I5IkkE= @@ -153,8 +155,8 @@ github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoG github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= -github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= -github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/btcutil v1.2.0 h1:p3+S2g3Q+7G5NOh4Ji+2UrBOrg5Z0Q4ykzShWG1Dhgs= +github.com/btcsuite/btcd/btcutil v1.2.0/go.mod h1:/Taflm113pYjUpbWKKQEfa6XOtI/+WS8awxeMZpY75k= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= @@ -182,6 +184,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -217,8 +221,8 @@ github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/c github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g= github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.39.3 h1:UegHXskZNomsijmm29nL5NkeXtnzkme6fg+q1hPQnEI= -github.com/cometbft/cometbft v0.39.3/go.mod h1:PmNfvtw256BC41ad0FABts236CSZnvZ0kjPOciBwTdM= +github.com/cometbft/cometbft v0.39.0-rc1.0.20260615134937-9ea34470f336 h1:Nyc8hg/+dXwmSrilPEmGJS/qMgybivfkGa8JMdV1vkQ= +github.com/cometbft/cometbft v0.39.0-rc1.0.20260615134937-9ea34470f336/go.mod h1:JxBvpWV9MU+ZqHdZaZmuFfrqgqyMGV8AYJWhJ/nPPh4= github.com/cometbft/cometbft-db v1.0.4 h1:cezb8yx/ZWcF124wqUtAFjAuDksS1y1yXedvtprUFxs= github.com/cometbft/cometbft-db v1.0.4/go.mod h1:M+BtHAGU2XLrpUxo3Nn1nOCcnVCiLM9yx5OuT0u5SCA= github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= @@ -296,6 +300,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deliveryhero/pipeline/v2 v2.2.0 h1:l9F+e4Q3qMu0zw7fi0JVg2FilgnGyohnwJ5d1oBqoYs= +github.com/deliveryhero/pipeline/v2 v2.2.0/go.mod h1:GghgCAlOoG8IdwybJpe2E3LBTuR+o5wgTdm0kPv3GPU= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/desertbit/timer v1.0.1 h1:yRpYNn5Vaaj6QXecdLMPMJsW81JLiI1eokUft5nBmeo= github.com/desertbit/timer v1.0.1/go.mod h1:htRrYeY5V/t4iu1xCJ5XsQvp4xve8QulXXctAzxqcwE= @@ -381,6 +387,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -409,6 +417,8 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -416,6 +426,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= @@ -482,6 +494,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= @@ -601,6 +615,14 @@ github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7 github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= @@ -699,6 +721,8 @@ github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= @@ -795,6 +819,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -914,6 +940,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -957,6 +985,8 @@ github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351P github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -970,6 +1000,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -1051,6 +1083,8 @@ github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI= github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE= +github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0 h1:8fdv/9y3JMxjQ+ULAcOG8RtgeNu5t9XF9LolSXDuTwM= +github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0/go.mod h1:CFr2LncGYokw+OKjXcr8ARCKG1SaC2UEnGxFBovE86g= github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= @@ -1497,6 +1531,34 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/e2e/internal/harness/ibclink/relayer_config.go b/e2e/internal/harness/ibclink/relayer_config.go index 7997f3e27..c5fb8d963 100644 --- a/e2e/internal/harness/ibclink/relayer_config.go +++ b/e2e/internal/harness/ibclink/relayer_config.go @@ -48,10 +48,14 @@ type RelayerChain struct { // RelayerConnection is a reciprocal on-chain client pair. Clients are the // registered client identifiers (locators). type RelayerConnection struct { - ChainA string - ClientA string - ChainB string - ClientB string + ChainA string + ClientA string + ClientAType string + ClientAParams map[string]any + ChainB string + ClientB string + ClientBType string + ClientBParams map[string]any } // RelayerAttestor describes one candidate attestor: a local entry runs in @@ -155,19 +159,29 @@ func buildRelayerFileConfig(cfg RelayerConfig) (fileConfig, error) { } for _, connection := range cfg.Connections { + clientAType := connection.ClientAType + if clientAType == "" { + clientAType = "attestation" + } + clientBType := connection.ClientBType + if clientBType == "" { + clientBType = "attestation" + } file.Relayer.Connections = append(file.Relayer.Connections, connectionFileConfig{ Alias: connection.ClientA + "-" + connection.ClientB, ClientA: clientEndFileConfig{ - ChainID: connection.ChainA, - Signer: cfg.SignerAlias, - ClientID: connection.ClientA, - Type: "attestation", + ChainID: connection.ChainA, + Signer: cfg.SignerAlias, + ClientID: connection.ClientA, + Type: clientAType, + ClientParams: connection.ClientAParams, }, ClientB: clientEndFileConfig{ - ChainID: connection.ChainB, - Signer: cfg.SignerAlias, - ClientID: connection.ClientB, - Type: "attestation", + ChainID: connection.ChainB, + Signer: cfg.SignerAlias, + ClientID: connection.ClientB, + Type: clientBType, + ClientParams: connection.ClientBParams, }, }) } @@ -253,8 +267,9 @@ type connectionFileConfig struct { } type clientEndFileConfig struct { - ChainID string `yaml:"chainId"` - Signer string `yaml:"signer"` - ClientID string `yaml:"clientId"` - Type string `yaml:"type"` + ChainID string `yaml:"chainId"` + Signer string `yaml:"signer"` + ClientID string `yaml:"clientId"` + Type string `yaml:"type"` + ClientParams map[string]any `yaml:"clientParams,omitempty"` } diff --git a/e2e/test-matrix.md b/e2e/test-matrix.md index cdede660f..09b750eba 100644 --- a/e2e/test-matrix.md +++ b/e2e/test-matrix.md @@ -36,6 +36,7 @@ | `TestRelay_FilteredSequences` | EVM portable | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Besu; 2 IBC instances; 1 connection; 2 attestors | | `TestRelay_FilteredTimeoutSequences` | EVM portable | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Besu; 2 IBC instances; 1 connection; 2 attestors | | `TestRelayerRecoversAfterNodeRestart` | EVM (node lifecycle) | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | +| `TestRemoteAttestationLightClientRelaysPacket` | EVM portable | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Besu; 2 IBC instances; 1 connection; 2 attestors | | `TestRemoteSignerFixtureRequiresKeyID` | None | No environment | No environment | No environment | | `TestTransferTimeout_Refund` | EVM (controlled mining) | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | | `TestTransfer_AutoRelay` | EVM portable | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Anvil; 2 IBC instances; 1 connection; 2 attestors | 2× Besu; 2 IBC instances; 1 connection; 2 attestors | diff --git a/link/.mockery.yaml b/link/.mockery.yaml index c73a092c9..50e671f8f 100644 --- a/link/.mockery.yaml +++ b/link/.mockery.yaml @@ -44,7 +44,7 @@ packages: pkgname: "mocks" structname: "Mock{{.InterfaceName}}" interfaces: - ProofGenerator: + Prover: github.com/cosmos/ibc/link/internal/relay/txbuilder: config: dir: "internal/tests/mocks" diff --git a/link/cmd/ibc/AGENTS.md b/link/cli/AGENTS.md similarity index 90% rename from link/cmd/ibc/AGENTS.md rename to link/cli/AGENTS.md index 2589452dc..5c237240c 100644 --- a/link/cmd/ibc/AGENTS.md +++ b/link/cli/AGENTS.md @@ -1,8 +1,8 @@ -# CLI development (located in cmd/ibc/) +# CLI development (located in cli/) -1. Use a single `init()` function in main.go to wire all subcommands. Never create init() per CLI file. +1. Use a single `init()` function in root.go to wire all subcommands. Never create init() per CLI file. 2. Commands implemented in this package should be variables and start with a `cmd*` prefix. The relayer family is constructed by an importable command package because its transport types are shared with the e2e harness. Examples for commands that remain here: diff --git a/link/cmd/ibc/attestor.go b/link/cli/attestor.go similarity index 99% rename from link/cmd/ibc/attestor.go rename to link/cli/attestor.go index 550990ec7..438948765 100644 --- a/link/cmd/ibc/attestor.go +++ b/link/cli/attestor.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "context" diff --git a/link/cmd/ibc/attestor_test.go b/link/cli/attestor_test.go similarity index 99% rename from link/cmd/ibc/attestor_test.go rename to link/cli/attestor_test.go index db69cff25..d03edd7ce 100644 --- a/link/cmd/ibc/attestor_test.go +++ b/link/cli/attestor_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "testing" diff --git a/link/cmd/ibc/attestors.go b/link/cli/attestors.go similarity index 99% rename from link/cmd/ibc/attestors.go rename to link/cli/attestors.go index 2a981b624..4c7578a5f 100644 --- a/link/cmd/ibc/attestors.go +++ b/link/cli/attestors.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "github.com/pkg/errors" diff --git a/link/cmd/ibc/attestors_test.go b/link/cli/attestors_test.go similarity index 99% rename from link/cmd/ibc/attestors_test.go rename to link/cli/attestors_test.go index 0d523d459..bdca1c1f2 100644 --- a/link/cmd/ibc/attestors_test.go +++ b/link/cli/attestors_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "path/filepath" diff --git a/link/cmd/ibc/config.go b/link/cli/config.go similarity index 99% rename from link/cmd/ibc/config.go rename to link/cli/config.go index 25ea9aee8..50d01d84c 100644 --- a/link/cmd/ibc/config.go +++ b/link/cli/config.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "fmt" diff --git a/link/cmd/ibc/config_test.go b/link/cli/config_test.go similarity index 97% rename from link/cmd/ibc/config_test.go rename to link/cli/config_test.go index 3034a3a85..4b05aad97 100644 --- a/link/cmd/ibc/config_test.go +++ b/link/cli/config_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "path/filepath" diff --git a/link/cmd/ibc/deploy.go b/link/cli/deploy.go similarity index 99% rename from link/cmd/ibc/deploy.go rename to link/cli/deploy.go index 2bac07d54..72377c261 100644 --- a/link/cmd/ibc/deploy.go +++ b/link/cli/deploy.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "bufio" diff --git a/link/cmd/ibc/deploy_test.go b/link/cli/deploy_test.go similarity index 99% rename from link/cmd/ibc/deploy_test.go rename to link/cli/deploy_test.go index dd0d8171f..315fd4159 100644 --- a/link/cmd/ibc/deploy_test.go +++ b/link/cli/deploy_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "io" diff --git a/link/cmd/ibc/ift.go b/link/cli/ift.go similarity index 99% rename from link/cmd/ibc/ift.go rename to link/cli/ift.go index 18d1f6147..b369a9077 100644 --- a/link/cmd/ibc/ift.go +++ b/link/cli/ift.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "context" diff --git a/link/cmd/ibc/ift_test.go b/link/cli/ift_test.go similarity index 97% rename from link/cmd/ibc/ift_test.go rename to link/cli/ift_test.go index 424aa3640..f04bb5abf 100644 --- a/link/cmd/ibc/ift_test.go +++ b/link/cli/ift_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "math/big" diff --git a/link/cmd/ibc/keys.go b/link/cli/keys.go similarity index 99% rename from link/cmd/ibc/keys.go rename to link/cli/keys.go index 2288b2287..ba528d5e3 100644 --- a/link/cmd/ibc/keys.go +++ b/link/cli/keys.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "encoding/base64" diff --git a/link/cmd/ibc/migrate.go b/link/cli/migrate.go similarity index 99% rename from link/cmd/ibc/migrate.go rename to link/cli/migrate.go index f4e997130..ed7a924f7 100644 --- a/link/cmd/ibc/migrate.go +++ b/link/cli/migrate.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "context" diff --git a/link/cmd/ibc/query.go b/link/cli/query.go similarity index 93% rename from link/cmd/ibc/query.go rename to link/cli/query.go index 240d98d12..e85e8cbb1 100644 --- a/link/cmd/ibc/query.go +++ b/link/cli/query.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "github.com/spf13/cobra" diff --git a/link/cmd/ibc/relayer.go b/link/cli/relayer.go similarity index 95% rename from link/cmd/ibc/relayer.go rename to link/cli/relayer.go index b7dc7b43d..285218eb9 100644 --- a/link/cmd/ibc/relayer.go +++ b/link/cli/relayer.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "context" @@ -43,6 +43,7 @@ var ( ) var ( + relayerOptions RelayerOptions flagRelayerNoMigrate bool flagRelayerHost string flagRelayerTxHash string @@ -55,7 +56,9 @@ func relayerRun(cmd *cobra.Command, _ []string) error { return err } - app, err := bootstrap.BuildRelayer(cfg) + app, err := bootstrap.BuildRelayer(cfg, bootstrap.RelayerOptions{ + ProverFactories: relayerOptions.ProverFactories, + }) if err != nil { return err } diff --git a/link/cli/root.go b/link/cli/root.go new file mode 100644 index 000000000..bda29d918 --- /dev/null +++ b/link/cli/root.go @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 + +package cli + +import ( + "context" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/spf13/cobra" + + "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/internal/deploy" + "github.com/cosmos/ibc/link/internal/pkg/logging" + "github.com/cosmos/ibc/link/lightclient" +) + +// Options configures the CLI. +type Options struct { + Relayer RelayerOptions +} + +// RelayerOptions configures relayer commands. +type RelayerOptions struct { + // ProverFactories contains custom prover factories. + ProverFactories *lightclient.Registry +} + +// global globalFlags, loaded in config.DeclarePersistentFlags() +var globalFlags = config.DefaultFlagSet() + +// useStatus is the shared "status" subcommand name and status-field key, +// factored out to satisfy goconst across cmd/ibc. +const useStatus = "status" + +// useIFT is the shared "ift" subcommand name, factored out to satisfy +// goconst across cmd/ibc. +const useIFT = "ift" + +var rootCmd = &cobra.Command{ + Use: "ibc", + Short: "IBC Link", +} + +// NewRootCmd constructs the IBC CLI. +func NewRootCmd(opts Options) *cobra.Command { + relayerOptions = opts.Relayer + + return rootCmd +} + +// Execute runs root and returns its exit code. +func Execute(root *cobra.Command) int { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + if err := root.ExecuteContext(ctx); err != nil { + return 1 + } + + return 0 +} + +// single init() for binding all commands to rootCmd +func init() { + // setup global flags + config.DeclarePersistentFlags(rootCmd, &globalFlags) + + cobra.OnInitialize(func() { + slog.SetDefault(logging.Default(globalFlags.LogJSON)) + }) + + rootCmd.AddCommand( + cmdConfig, + cmdRelayer, + cmdAttestor, + cmdQuery, + cmdMigrate, + cmdKeys, + cmdDeploy, + cmdTx, + ) + + cmdConfig.AddCommand(cmdConfigNew, cmdConfigValidate, cmdConfigAddChain) + cmdConfigNew.Flags().BoolVar(&flagConfigNewOut, "out", false, "output the config to stdout") + cmdConfigValidate.Flags().BoolVar(&flagConfigValidateLive, "live", false, "extra validation checks") + cmdConfigValidate.Flags(). + BoolVar(&flagConfigValidateStrict, "strict", false, "fail on unknown fields in the config file") + cmdConfigAddChain.Flags().StringVar(&flagConfigAddChainID, "chain-id", "", "chain ID") + cmdConfigAddChain.Flags().StringVar(&flagConfigAddChainRPC, "rpc", "", "chain RPC URL") + cmdConfigAddChain.Flags(). + StringVar(&flagConfigAddChainRouter, "router", "", "ics26Router address (default: left blank, fill in via render-config)") + cmdConfigAddChain.Flags(). + StringVar(&flagConfigAddChainDeployer, "deployer", "", "signer alias used by ibc deploy for this chain") + for _, req := range []string{"chain-id", "rpc"} { + _ = cmdConfigAddChain.MarkFlagRequired(req) + } + + // Keys commands + cmdKeys.AddCommand(cmdKeysNew, cmdKeysShow, cmdKeysImport, cmdKeysList) + cmdKeysShow.Flags().BoolVarP(&flagKeysShowPrivate, "private", "", false, "show private key") + cmdKeysImport.Flags().StringVar(&flagKeysImportPrivateKey, "private-key", "", "hex-encoded private key") + for _, c := range []*cobra.Command{cmdKeysNew, cmdKeysImport} { + c.Flags(). + BoolVar(&flagKeysPopulateConfig, "populate-config", false, "append the resulting key as a signers entry in the config file") + } + + // Relayer commands + cmdRelayer.AddCommand(cmdRelayerRun, cmdRelayerRelay, cmdRelayerStatus) + cmdRelayerRun.Flags().BoolVarP(&flagRelayerNoMigrate, "no-migrate", "", false, "skip database migrations") + for _, c := range []*cobra.Command{cmdRelayerRelay, cmdRelayerStatus} { + c.Flags().StringVar(&flagRelayerHost, "host", "", "dial this address instead of resolving from config") + c.Flags().StringVar(&flagRelayerTxHash, "tx-hash", "", "source transaction hash") + c.Flags().StringVar(&flagRelayerSourceChainID, "chain-id", "", "source chain id") + _ = c.MarkFlagRequired("tx-hash") + _ = c.MarkFlagRequired("chain-id") + } + + // Attestor commands + cmdAttestor.AddCommand(cmdAttestorRun, cmdAttestorInfo, cmdAttestorLatestHeight, cmdAttestorStateAttestation) + for _, c := range []*cobra.Command{cmdAttestorInfo, cmdAttestorLatestHeight, cmdAttestorStateAttestation} { + c.Flags().StringVar(&flagAttestorHost, "host", "", "dial this address instead of resolving from config") + } + cmdAttestorStateAttestation.Flags().Uint64Var(&flagAttestorHeight, "height", 0, "height to attest") + + // Query commands + cmdQuery.AddCommand(cmdQueryIFT) + cmdQueryIFT.AddCommand(cmdQueryIFTBalance) + qpf := cmdQueryIFT.PersistentFlags() + qpf.StringVar(&flagQueryIFTChain, "chain", "", "chain ID the IFT token is deployed on") + qpf.StringVar(&flagQueryIFTAddress, "ift", "", "IFT token address") + for _, req := range []string{"chain", useIFT} { + _ = cmdQueryIFT.MarkPersistentFlagRequired(req) + } + cmdQueryIFTBalance.Flags(). + StringVar(&flagQueryIFTAccount, "address", "", "account address, or a configured signer alias") + _ = cmdQueryIFTBalance.MarkFlagRequired("address") + + // Migrate commands + cmdMigrate.AddCommand(cmdMigrateUp, cmdMigrateDown, cmdMigrateStatus) + + // Deploy commands + cmdDeploy.AddCommand( + cmdDeployCore, cmdDeployClient, + cmdDeployStatus, cmdDeployShow, cmdDeployRenderConfig, + cmdDeployGMP, cmdDeployIFT, cmdDeployIFTBridge, + ) + dpf := cmdDeploy.PersistentFlags() + dpf.StringVar(&flagDeployManifestDir, "manifest-dir", "deployments", "manifest directory relative to home") + dpf.StringVar(&flagDeployDeployer, "deployer", "", "signer alias override for deployment transactions") + dpf.StringVar(&flagDeployChain, "chain", "", "chain ID for the chain being deployed to") + dpf.BoolVar(&flagDeployDryRun, "dry-run", false, "print the step plan without submitting transactions") + dpf.BoolVar(&flagDeployYes, "yes", false, "skip confirmation prompts") + + cmdDeployClient.Flags(). + StringVar(&flagDeployCounterparty, "counterparty-chain", "", "counterparty chain id the client tracks") + _ = cmdDeployClient.MarkFlagRequired("counterparty-chain") + cmdDeployClient.Flags().StringVar(&flagDeployClientType, "type", deploy.ClientTypeAttestation, "light client type") + cmdDeployClient.Flags(). + StringSliceVar(&flagDeployAttestors, "attestors", nil, + "attestors for the new client: addresses, attestation names, or signer aliases (default: configured attestations for the tracked chain)") + cmdDeployClient.Flags().Uint8Var(&flagDeployThreshold, "threshold", 1, "attestation signature threshold") + cmdDeployClient.Flags(). + StringVar(&flagDeployClientID, "client-id", "", "client id (default: link--, chain ids sorted)") + cmdDeployClient.Flags(). + StringVar(&flagDeployCounterpartyCID, "counterparty-client-id", "", "counterparty's client id (default: link--, chain ids sorted)") + cmdDeployClient.Flags(). + Uint64Var(&flagDeployHeight, "height", 0, "initial trusted height (default: counterparty head)") + cmdDeployClient.Flags(). + Uint64Var(&flagDeployTimestamp, "timestamp", 0, "initial trusted timestamp seconds (default: counterparty head)") + + // IFT commands + cmdDeployIFT.Flags().StringVar(&flagDeployIFTName, "name", "", "ERC20 token name") + cmdDeployIFT.Flags().StringVar(&flagDeployIFTSymbol, "symbol", "", "ERC20 token symbol (need not be unique)") + cmdDeployIFT.Flags().StringVar(&flagDeployIFTOwner, "owner", "", "token owner address (default: deployer)") + _ = cmdDeployIFT.MarkFlagRequired("name") + _ = cmdDeployIFT.MarkFlagRequired("symbol") + + cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeChainA, "chain-a", "", "first chain id") + cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeIFTA, "ift-a", "", "IFT token address on chain A") + cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeChainB, "chain-b", "", "second chain id") + cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeIFTB, "ift-b", "", "IFT token address on chain B") + for _, req := range []string{"chain-a", "ift-a", "chain-b", "ift-b"} { + _ = cmdDeployIFTBridge.MarkFlagRequired(req) + } + cmdDeployIFTBridge.Flags(). + StringVar(&flagDeployBridgeClientID, "client-id", "", "client id the bridge relays over (default: link--)") + cmdDeployIFTBridge.Flags(). + StringVar(&flagDeployBridgeCtorA, "send-call-constructor-a", "", + "send call constructor address on chain A (default: deploy or reuse the EVM constructor)") + cmdDeployIFTBridge.Flags(). + StringVar(&flagDeployBridgeCtorB, "send-call-constructor-b", "", + "send call constructor address on chain B (default: deploy or reuse the EVM constructor)") + + // Tx commands + cmdTx.AddCommand(cmdTxIFT) + cmdTxIFT.AddCommand(cmdTxIFTMint, cmdTxIFTSend) + tpf := cmdTxIFT.PersistentFlags() + tpf.StringVar(&flagTxIFTChain, "chain", "", "chain ID the IFT token is deployed on") + tpf.StringVar(&flagTxIFTAddress, "ift", "", "IFT token address") + tpf.StringVar(&flagTxIFTFrom, "from", "", "signer alias to submit the transaction with") + for _, req := range []string{"chain", useIFT, "from"} { + _ = cmdTxIFT.MarkPersistentFlagRequired(req) + } + cmdTxIFTMint.Flags().StringVar(&flagTxIFTTo, "to", "", "recipient address, or a configured signer alias") + cmdTxIFTMint.Flags().StringVar(&flagTxIFTAmount, "amount", "", "amount to mint, in the token's base unit") + for _, req := range []string{"to", "amount"} { + _ = cmdTxIFTMint.MarkFlagRequired(req) + } + cmdTxIFTSend.Flags().StringVar(&flagTxIFTClientID, "client-id", "", "client id the bridge is registered for") + cmdTxIFTSend.Flags(). + StringVar(&flagTxIFTTo, "to", "", "receiver address on the counterparty chain, or a configured signer alias") + cmdTxIFTSend.Flags().StringVar(&flagTxIFTAmount, "amount", "", "amount to send, in the token's base unit") + cmdTxIFTSend.Flags(). + DurationVar(&flagTxIFTTimeout, "timeout", 15*time.Minute, "relative send timeout") + for _, req := range []string{"client-id", "to", "amount"} { + _ = cmdTxIFTSend.MarkFlagRequired(req) + } +} diff --git a/link/cmd/ibc/tx.go b/link/cli/tx.go similarity index 92% rename from link/cmd/ibc/tx.go rename to link/cli/tx.go index 8ff85e644..ca61233f9 100644 --- a/link/cmd/ibc/tx.go +++ b/link/cli/tx.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package main +package cli import ( "github.com/spf13/cobra" diff --git a/link/cmd/ibc/main.go b/link/cmd/ibc/main.go index 80c05851a..90776f272 100644 --- a/link/cmd/ibc/main.go +++ b/link/cmd/ibc/main.go @@ -3,202 +3,11 @@ package main import ( - "context" - "log/slog" "os" - "os/signal" - "syscall" - "time" - "github.com/spf13/cobra" - - "github.com/cosmos/ibc/link/internal/config" - "github.com/cosmos/ibc/link/internal/deploy" - "github.com/cosmos/ibc/link/internal/pkg/logging" + "github.com/cosmos/ibc/link/cli" ) -// global globalFlags, loaded in config.DeclarePersistentFlags() -var globalFlags = config.DefaultFlagSet() - -// useStatus is the shared "status" subcommand name and status-field key, -// factored out to satisfy goconst across cmd/ibc. -const useStatus = "status" - -// useIFT is the shared "ift" subcommand name, factored out to satisfy -// goconst across cmd/ibc. -const useIFT = "ift" - -var rootCmd = &cobra.Command{ - Use: "ibc", - Short: "IBC Link", -} - func main() { - os.Exit(runMain()) -} - -func runMain() int { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() - if err := rootCmd.ExecuteContext(ctx); err != nil { - return 1 - } - return 0 -} - -// single init() for binding all commands to rootCmd -func init() { - // setup global flags - config.DeclarePersistentFlags(rootCmd, &globalFlags) - - cobra.OnInitialize(func() { - slog.SetDefault(logging.Default(globalFlags.LogJSON)) - }) - - rootCmd.AddCommand( - cmdConfig, - cmdRelayer, - cmdAttestor, - cmdQuery, - cmdMigrate, - cmdKeys, - cmdDeploy, - cmdTx, - ) - - cmdConfig.AddCommand(cmdConfigNew, cmdConfigValidate, cmdConfigAddChain) - cmdConfigNew.Flags().BoolVar(&flagConfigNewOut, "out", false, "output the config to stdout") - cmdConfigValidate.Flags().BoolVar(&flagConfigValidateLive, "live", false, "extra validation checks") - cmdConfigValidate.Flags(). - BoolVar(&flagConfigValidateStrict, "strict", false, "fail on unknown fields in the config file") - cmdConfigAddChain.Flags().StringVar(&flagConfigAddChainID, "chain-id", "", "chain ID") - cmdConfigAddChain.Flags().StringVar(&flagConfigAddChainRPC, "rpc", "", "chain RPC URL") - cmdConfigAddChain.Flags(). - StringVar(&flagConfigAddChainRouter, "router", "", "ics26Router address (default: left blank, fill in via render-config)") - cmdConfigAddChain.Flags(). - StringVar(&flagConfigAddChainDeployer, "deployer", "", "signer alias used by ibc deploy for this chain") - for _, req := range []string{"chain-id", "rpc"} { - _ = cmdConfigAddChain.MarkFlagRequired(req) - } - - // Keys commands - cmdKeys.AddCommand(cmdKeysNew, cmdKeysShow, cmdKeysImport, cmdKeysList) - cmdKeysShow.Flags().BoolVarP(&flagKeysShowPrivate, "private", "", false, "show private key") - cmdKeysImport.Flags().StringVar(&flagKeysImportPrivateKey, "private-key", "", "hex-encoded private key") - for _, c := range []*cobra.Command{cmdKeysNew, cmdKeysImport} { - c.Flags(). - BoolVar(&flagKeysPopulateConfig, "populate-config", false, "append the resulting key as a signers entry in the config file") - } - - // Relayer commands - cmdRelayer.AddCommand(cmdRelayerRun, cmdRelayerRelay, cmdRelayerStatus) - cmdRelayerRun.Flags().BoolVarP(&flagRelayerNoMigrate, "no-migrate", "", false, "skip database migrations") - for _, c := range []*cobra.Command{cmdRelayerRelay, cmdRelayerStatus} { - c.Flags().StringVar(&flagRelayerHost, "host", "", "dial this address instead of resolving from config") - c.Flags().StringVar(&flagRelayerTxHash, "tx-hash", "", "source transaction hash") - c.Flags().StringVar(&flagRelayerSourceChainID, "chain-id", "", "source chain id") - _ = c.MarkFlagRequired("tx-hash") - _ = c.MarkFlagRequired("chain-id") - } - - // Attestor commands - cmdAttestor.AddCommand(cmdAttestorRun, cmdAttestorInfo, cmdAttestorLatestHeight, cmdAttestorStateAttestation) - for _, c := range []*cobra.Command{cmdAttestorInfo, cmdAttestorLatestHeight, cmdAttestorStateAttestation} { - c.Flags().StringVar(&flagAttestorHost, "host", "", "dial this address instead of resolving from config") - } - cmdAttestorStateAttestation.Flags().Uint64Var(&flagAttestorHeight, "height", 0, "height to attest") - - // Query commands - cmdQuery.AddCommand(cmdQueryIFT) - cmdQueryIFT.AddCommand(cmdQueryIFTBalance) - qpf := cmdQueryIFT.PersistentFlags() - qpf.StringVar(&flagQueryIFTChain, "chain", "", "chain ID the IFT token is deployed on") - qpf.StringVar(&flagQueryIFTAddress, "ift", "", "IFT token address") - for _, req := range []string{"chain", useIFT} { - _ = cmdQueryIFT.MarkPersistentFlagRequired(req) - } - cmdQueryIFTBalance.Flags(). - StringVar(&flagQueryIFTAccount, "address", "", "account address, or a configured signer alias") - _ = cmdQueryIFTBalance.MarkFlagRequired("address") - - // Migrate commands - cmdMigrate.AddCommand(cmdMigrateUp, cmdMigrateDown, cmdMigrateStatus) - - // Deploy commands - cmdDeploy.AddCommand( - cmdDeployCore, cmdDeployClient, - cmdDeployStatus, cmdDeployShow, cmdDeployRenderConfig, - cmdDeployGMP, cmdDeployIFT, cmdDeployIFTBridge, - ) - dpf := cmdDeploy.PersistentFlags() - dpf.StringVar(&flagDeployManifestDir, "manifest-dir", "deployments", "manifest directory relative to home") - dpf.StringVar(&flagDeployDeployer, "deployer", "", "signer alias override for deployment transactions") - dpf.StringVar(&flagDeployChain, "chain", "", "chain ID for the chain being deployed to") - dpf.BoolVar(&flagDeployDryRun, "dry-run", false, "print the step plan without submitting transactions") - dpf.BoolVar(&flagDeployYes, "yes", false, "skip confirmation prompts") - - cmdDeployClient.Flags(). - StringVar(&flagDeployCounterparty, "counterparty-chain", "", "counterparty chain id the client tracks") - _ = cmdDeployClient.MarkFlagRequired("counterparty-chain") - cmdDeployClient.Flags().StringVar(&flagDeployClientType, "type", deploy.ClientTypeAttestation, "light client type") - cmdDeployClient.Flags(). - StringSliceVar(&flagDeployAttestors, "attestors", nil, - "attestors for the new client: addresses, attestation names, or signer aliases (default: configured attestations for the tracked chain)") - cmdDeployClient.Flags().Uint8Var(&flagDeployThreshold, "threshold", 1, "attestation signature threshold") - cmdDeployClient.Flags(). - StringVar(&flagDeployClientID, "client-id", "", "client id (default: link--, chain ids sorted)") - cmdDeployClient.Flags(). - StringVar(&flagDeployCounterpartyCID, "counterparty-client-id", "", "counterparty's client id (default: link--, chain ids sorted)") - cmdDeployClient.Flags(). - Uint64Var(&flagDeployHeight, "height", 0, "initial trusted height (default: counterparty head)") - cmdDeployClient.Flags(). - Uint64Var(&flagDeployTimestamp, "timestamp", 0, "initial trusted timestamp seconds (default: counterparty head)") - - // IFT commands - cmdDeployIFT.Flags().StringVar(&flagDeployIFTName, "name", "", "ERC20 token name") - cmdDeployIFT.Flags().StringVar(&flagDeployIFTSymbol, "symbol", "", "ERC20 token symbol (need not be unique)") - cmdDeployIFT.Flags().StringVar(&flagDeployIFTOwner, "owner", "", "token owner address (default: deployer)") - _ = cmdDeployIFT.MarkFlagRequired("name") - _ = cmdDeployIFT.MarkFlagRequired("symbol") - - cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeChainA, "chain-a", "", "first chain id") - cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeIFTA, "ift-a", "", "IFT token address on chain A") - cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeChainB, "chain-b", "", "second chain id") - cmdDeployIFTBridge.Flags().StringVar(&flagDeployBridgeIFTB, "ift-b", "", "IFT token address on chain B") - for _, req := range []string{"chain-a", "ift-a", "chain-b", "ift-b"} { - _ = cmdDeployIFTBridge.MarkFlagRequired(req) - } - cmdDeployIFTBridge.Flags(). - StringVar(&flagDeployBridgeClientID, "client-id", "", "client id the bridge relays over (default: link--)") - cmdDeployIFTBridge.Flags(). - StringVar(&flagDeployBridgeCtorA, "send-call-constructor-a", "", - "send call constructor address on chain A (default: deploy or reuse the EVM constructor)") - cmdDeployIFTBridge.Flags(). - StringVar(&flagDeployBridgeCtorB, "send-call-constructor-b", "", - "send call constructor address on chain B (default: deploy or reuse the EVM constructor)") - - // Tx commands - cmdTx.AddCommand(cmdTxIFT) - cmdTxIFT.AddCommand(cmdTxIFTMint, cmdTxIFTSend) - tpf := cmdTxIFT.PersistentFlags() - tpf.StringVar(&flagTxIFTChain, "chain", "", "chain ID the IFT token is deployed on") - tpf.StringVar(&flagTxIFTAddress, "ift", "", "IFT token address") - tpf.StringVar(&flagTxIFTFrom, "from", "", "signer alias to submit the transaction with") - for _, req := range []string{"chain", useIFT, "from"} { - _ = cmdTxIFT.MarkPersistentFlagRequired(req) - } - cmdTxIFTMint.Flags().StringVar(&flagTxIFTTo, "to", "", "recipient address, or a configured signer alias") - cmdTxIFTMint.Flags().StringVar(&flagTxIFTAmount, "amount", "", "amount to mint, in the token's base unit") - for _, req := range []string{"to", "amount"} { - _ = cmdTxIFTMint.MarkFlagRequired(req) - } - cmdTxIFTSend.Flags().StringVar(&flagTxIFTClientID, "client-id", "", "client id the bridge is registered for") - cmdTxIFTSend.Flags(). - StringVar(&flagTxIFTTo, "to", "", "receiver address on the counterparty chain, or a configured signer alias") - cmdTxIFTSend.Flags().StringVar(&flagTxIFTAmount, "amount", "", "amount to send, in the token's base unit") - cmdTxIFTSend.Flags(). - DurationVar(&flagTxIFTTimeout, "timeout", 15*time.Minute, "relative send timeout") - for _, req := range []string{"client-id", "to", "amount"} { - _ = cmdTxIFTSend.MarkFlagRequired(req) - } + os.Exit(cli.Execute(cli.NewRootCmd(cli.Options{}))) } diff --git a/link/internal/bootstrap/bootstrap.go b/link/internal/bootstrap/bootstrap.go index 3abb7c57f..e2fbea4f4 100644 --- a/link/internal/bootstrap/bootstrap.go +++ b/link/internal/bootstrap/bootstrap.go @@ -18,6 +18,7 @@ import ( "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/txsubmitter" + "github.com/cosmos/ibc/link/lightclient" ) // Services is an outcome of IBC Link wiring (dep inject) @@ -33,8 +34,14 @@ type Services struct { AttestorService *attestor.Service } -// BuildRelayer converts config into a runnable relayer process with all of the deps provisioned -func BuildRelayer(cfg config.Config) (*Services, error) { +// RelayerOptions supplies optional relayer extensions. +type RelayerOptions struct { + // ProverFactories contains custom prover factories. + ProverFactories *lightclient.Registry +} + +// BuildRelayer converts config into a runnable relayer process with all of the deps provisioned. +func BuildRelayer(cfg config.Config, opts RelayerOptions) (*Services, error) { ctx := context.Background() logger := slog.With("module", "bootstrap") @@ -79,7 +86,9 @@ func BuildRelayer(cfg config.Config) (*Services, error) { } // Proof generators - proofGenerators, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, append(local, remote...)) + proofGenerators, err := proofgen.NewSetFromConfig( + ctx, cfg, clientSet, append(local, remote...), opts.ProverFactories, + ) if err != nil { return nil, err } diff --git a/link/internal/config/relayer.go b/link/internal/config/relayer.go index e0ea9e183..895d149ff 100644 --- a/link/internal/config/relayer.go +++ b/link/internal/config/relayer.go @@ -6,6 +6,8 @@ import ( "time" "github.com/pkg/errors" + + "github.com/cosmos/ibc/link/lightclient" ) // ClientType the light client type. @@ -64,6 +66,9 @@ type ClientEnd struct { ClientID string `yaml:"clientId"` Type ClientType `yaml:"type"` + // ClientParams is interpreted by the factory registered for Type. + ClientParams *lightclient.RawParams `yaml:"clientParams,omitempty"` + // AutoRelay configures auto-relay for packets flowing FROM this end's // chain TOWARD the counterparty end. AutoRelay AutoRelayConfig `yaml:"autoRelay,omitempty"` @@ -185,8 +190,8 @@ func (c ClientEnd) Validate() error { return errors.New(".clientId required") case c.Signer == "": return errors.New(".signer required") - case c.Type != ClientTypeAttestation: - return errors.Errorf(".type unknown client type: %q", c.Type) + case c.Type == "": + return errors.New(".type required") } return nil diff --git a/link/internal/config/relayer_test.go b/link/internal/config/relayer_test.go index 34ea644cd..1777ef8f6 100644 --- a/link/internal/config/relayer_test.go +++ b/link/internal/config/relayer_test.go @@ -198,11 +198,19 @@ func TestRelayerConfig(t *testing.T) { errContains: ".chainId required", }, { - name: "unsupported client type", + // An unregistered type is not a structural config error. It is + // resolved when the relayer constructs provers. + name: "unregistered client type passes structural validation", patch: func(c *Config) { c.Relayer.Connections[0].ClientA.Type = "tendermint" }, - errContains: `unknown client type: "tendermint"`, + }, + { + name: "missing client type", + patch: func(c *Config) { + c.Relayer.Connections[0].ClientA.Type = "" + }, + errContains: ".type required", }, { name: "duplicate client", diff --git a/link/internal/livevalidate/quorum.go b/link/internal/livevalidate/quorum.go index a980bd204..cc37c5eae 100644 --- a/link/internal/livevalidate/quorum.go +++ b/link/internal/livevalidate/quorum.go @@ -33,7 +33,7 @@ func checkAttestorQuorum(ctx context.Context, cfg config.Config, clientSet *chai attestors = append(attestors, local...) attestors = append(attestors, remote...) - if _, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, attestors); err != nil { + if _, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, attestors, nil); err != nil { return errors.Wrap(err, "attestor quorum") } diff --git a/link/internal/livevalidate/quorum_test.go b/link/internal/livevalidate/quorum_test.go index 483ad8d89..b6eb8b55e 100644 --- a/link/internal/livevalidate/quorum_test.go +++ b/link/internal/livevalidate/quorum_test.go @@ -139,6 +139,7 @@ func TestCheckAttestorQuorum(t *testing.T) { cfg := config.Config{Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}} err := checkAttestorQuorum(ctx, cfg, chains.NewClientSet(nil)) - require.ErrorContains(t, err, `unsupported client type "tendermint"`) + require.ErrorContains(t, err, `no prover registered for client type "tendermint"`) + require.ErrorContains(t, err, "attestation", "the error should name what is registered") }) } diff --git a/link/internal/relay/dispatch/fixtures_test.go b/link/internal/relay/dispatch/fixtures_test.go index 4df3ba66f..dc0f1b63d 100644 --- a/link/internal/relay/dispatch/fixtures_test.go +++ b/link/internal/relay/dispatch/fixtures_test.go @@ -18,6 +18,7 @@ import ( "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/tests/mocks" "github.com/cosmos/ibc/link/internal/txsubmitter" + "github.com/cosmos/ibc/link/lightclient" ) func testTransfer(t *testing.T) *processors.Transfer { @@ -42,9 +43,9 @@ func (s staticChains) Get(chainID string) (chains.Client, bool) { return client, ok } -type staticProofGenerators map[string]proofgen.ProofGenerator +type staticProofGenerators map[string]lightclient.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.ProofGenerator, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (lightclient.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } @@ -70,8 +71,8 @@ func newPipelineEnv(t *testing.T) (*pipelineEnv, pipeline.Deps) { _, err = db.MigrateUp() require.NoError(t, err) - destProofGen := mocks.NewMockProofGenerator(t) - sourceProofGen := mocks.NewMockProofGenerator(t) + destProofGen := mocks.NewMockProver(t) + sourceProofGen := mocks.NewMockProver(t) deps := pipeline.Deps{ Storage: db, diff --git a/link/internal/relay/pipeline/pipeline_test.go b/link/internal/relay/pipeline/pipeline_test.go index c85070d82..6e72ca2ff 100644 --- a/link/internal/relay/pipeline/pipeline_test.go +++ b/link/internal/relay/pipeline/pipeline_test.go @@ -22,6 +22,7 @@ import ( "github.com/cosmos/ibc/link/internal/tests/mocks" "github.com/cosmos/ibc/link/internal/txsubmitter" v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) const ( @@ -44,8 +45,8 @@ type pipelineEnv struct { // dstProofGen/dstTxBuilder are resolved for the destination client/chain, // used by recv delivery; srcProofGen/srcTxBuilder are resolved for the // source client/chain, used by ack and timeout delivery. - dstProofGen *mocks.MockProofGenerator - srcProofGen *mocks.MockProofGenerator + dstProofGen *mocks.MockProver + srcProofGen *mocks.MockProver dstTxBuilder *mocks.MockTxBuilder srcTxBuilder *mocks.MockTxBuilder srcTxSubmitter *mocks.MockTxSubmitter @@ -59,9 +60,9 @@ func (s staticChains) Get(chainID string) (chains.Client, bool) { return client, ok } -type staticProofGenerators map[string]proofgen.ProofGenerator +type staticProofGenerators map[string]lightclient.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.ProofGenerator, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (lightclient.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } @@ -87,8 +88,8 @@ func newPipelineEnv(t *testing.T) (*pipelineEnv, Deps) { store: db, srcClient: mocks.NewMockClient(t), dstClient: mocks.NewMockClient(t), - dstProofGen: mocks.NewMockProofGenerator(t), - srcProofGen: mocks.NewMockProofGenerator(t), + dstProofGen: mocks.NewMockProver(t), + srcProofGen: mocks.NewMockProver(t), dstTxBuilder: mocks.NewMockTxBuilder(t), srcTxBuilder: mocks.NewMockTxBuilder(t), srcTxSubmitter: mocks.NewMockTxSubmitter(t), @@ -121,7 +122,7 @@ func newPipelineEnv(t *testing.T) (*pipelineEnv, Deps) { // for whichever chain the batch reads packet events from. func mockRelay( client *mocks.MockClient, - proofGen *mocks.MockProofGenerator, + proofGen *mocks.MockProver, txBuilder *mocks.MockTxBuilder, events []v2.PacketEvent, to string, diff --git a/link/internal/relay/processors/batch_ack_packet.go b/link/internal/relay/processors/batch_ack_packet.go index 6b82782b1..159e84063 100644 --- a/link/internal/relay/processors/batch_ack_packet.go +++ b/link/internal/relay/processors/batch_ack_packet.go @@ -11,11 +11,11 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/relay/txbuilder" "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/txsubmitter" v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) // BatchAckPacket delivers one ack tx on the source chain for a batch of @@ -24,7 +24,7 @@ type BatchAckPacket struct { destinationChainClient chains.Client sourceChainClient chains.Client route Route - proofGen proofgen.ProofGenerator + proofGen lightclient.Prover txBuilder txbuilder.TxBuilder txSubmitter txsubmitter.TxSubmitter storage TxStorage diff --git a/link/internal/relay/processors/batch_recv_packet.go b/link/internal/relay/processors/batch_recv_packet.go index 8b1e040f5..50e7e63a6 100644 --- a/link/internal/relay/processors/batch_recv_packet.go +++ b/link/internal/relay/processors/batch_recv_packet.go @@ -11,11 +11,11 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/relay/txbuilder" "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/txsubmitter" v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) // BatchRecvPacket delivers one recv tx on the destination chain for a batch @@ -24,7 +24,7 @@ type BatchRecvPacket struct { sourceChainClient chains.Client destinationChainClient chains.Client route Route - proofGen proofgen.ProofGenerator + proofGen lightclient.Prover txBuilder txbuilder.TxBuilder txSubmitter txsubmitter.TxSubmitter storage TxStorage diff --git a/link/internal/relay/processors/batch_recv_packet_test.go b/link/internal/relay/processors/batch_recv_packet_test.go index bf16b9891..6ed4422ba 100644 --- a/link/internal/relay/processors/batch_recv_packet_test.go +++ b/link/internal/relay/processors/batch_recv_packet_test.go @@ -21,6 +21,7 @@ import ( "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/tests/mocks" v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) type staticChains map[string]chains.Client @@ -30,9 +31,9 @@ func (s staticChains) Get(chainID string) (chains.Client, bool) { return client, ok } -type staticProofGenerators map[string]proofgen.ProofGenerator +type staticProofGenerators map[string]lightclient.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.ProofGenerator, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (lightclient.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } @@ -101,10 +102,10 @@ func TestBatchRecvPacketSequenceAlignment(t *testing.T) { }, nil }).Once() - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Time{}, nil) proofGen.EXPECT().StateProof(mock.Anything, uint64(100)).Return([]byte{0x01}, nil) - proofGen.EXPECT().PacketProofs(mock.Anything, uint64(100), v2.ProofKindPacketCommitment, mock.Anything). + proofGen.EXPECT().PacketProofs(mock.Anything, uint64(100), lightclient.ProofKindPacketCommitment, mock.Anything). Return([][]byte{{0x02}}, nil) txBuilder := mocks.NewMockTxBuilder(t) @@ -198,10 +199,10 @@ func TestBatchRecvPacketToleratesPartialEventFetchFailure(t *testing.T) { }, nil).Once() sourceChainClient.EXPECT().TxPacketEvents(mock.Anything, failingTxID).Return(nil, assert.AnError).Once() - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Time{}, nil) proofGen.EXPECT().StateProof(mock.Anything, uint64(100)).Return([]byte{0x01}, nil) - proofGen.EXPECT().PacketProofs(mock.Anything, uint64(100), v2.ProofKindPacketCommitment, mock.Anything). + proofGen.EXPECT().PacketProofs(mock.Anything, uint64(100), lightclient.ProofKindPacketCommitment, mock.Anything). Return([][]byte{{0x02}}, nil) txBuilder := mocks.NewMockTxBuilder(t) @@ -303,10 +304,10 @@ func TestBatchRecvPacketExcludesNotYetProvablePackets(t *testing.T) { }, }, nil).Once() - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Time{}, nil) proofGen.EXPECT().StateProof(mock.Anything, uint64(100)).Return([]byte{0x01}, nil) - proofGen.EXPECT().PacketProofs(mock.Anything, uint64(100), v2.ProofKindPacketCommitment, mock.Anything). + proofGen.EXPECT().PacketProofs(mock.Anything, uint64(100), lightclient.ProofKindPacketCommitment, mock.Anything). Return([][]byte{{0x02}}, nil) txBuilder := mocks.NewMockTxBuilder(t) diff --git a/link/internal/relay/processors/batch_relay.go b/link/internal/relay/processors/batch_relay.go index a20db2cee..323a5cc7f 100644 --- a/link/internal/relay/processors/batch_relay.go +++ b/link/internal/relay/processors/batch_relay.go @@ -10,10 +10,10 @@ import ( channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/relay/txbuilder" "github.com/cosmos/ibc/link/internal/txsubmitter" v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) // findPacketEvent returns the event among events matching sequence and clientID @@ -52,16 +52,16 @@ func findPacketEventAtOrBeforeHeight( } // proofKindFor maps relayKind to the proof claim it requires -func proofKindFor(relayKind v2.RelayKind) v2.ProofKind { +func proofKindFor(relayKind v2.RelayKind) lightclient.ProofKind { switch relayKind { case v2.RelayKindRecv: - return v2.ProofKindPacketCommitment + return lightclient.ProofKindPacketCommitment case v2.RelayKindAck: - return v2.ProofKindAcknowledgement + return lightclient.ProofKindAcknowledgement case v2.RelayKindTimeout: - return v2.ProofKindReceiptAbsence + return lightclient.ProofKindReceiptAbsence default: - return v2.ProofKindUnknown + return lightclient.ProofKindUnknown } } @@ -71,7 +71,7 @@ func proofKindFor(relayKind v2.RelayKind) v2.ProofKind { func relayPackets( ctx context.Context, chainClient chains.Client, - proofGen proofgen.ProofGenerator, + proofGen lightclient.Prover, txBuilder txbuilder.TxBuilder, txSubmitter txsubmitter.TxSubmitter, clientID string, diff --git a/link/internal/relay/processors/check_send_finality.go b/link/internal/relay/processors/check_send_finality.go index 4abc3f91a..562a55d0e 100644 --- a/link/internal/relay/processors/check_send_finality.go +++ b/link/internal/relay/processors/check_send_finality.go @@ -11,8 +11,8 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/store" + "github.com/cosmos/ibc/link/lightclient" ) // CheckSendFinality gates relaying on the send tx's height being at or @@ -20,7 +20,7 @@ import ( // prove. type CheckSendFinality struct { sourceChainClient chains.Client - proofGen proofgen.ProofGenerator + proofGen lightclient.Prover } func NewCheckSendFinality( diff --git a/link/internal/relay/processors/check_send_finality_test.go b/link/internal/relay/processors/check_send_finality_test.go index 4858f454d..319041ddb 100644 --- a/link/internal/relay/processors/check_send_finality_test.go +++ b/link/internal/relay/processors/check_send_finality_test.go @@ -32,7 +32,7 @@ func TestNewCheckSendFinality(t *testing.T) { _, err := NewCheckSendFinality( staticChains{}, staticProofGenerators{ - proofgen.Key(route.DestinationChainID, route.DestinationClientID): mocks.NewMockProofGenerator(t), + proofgen.Key(route.DestinationChainID, route.DestinationClientID): mocks.NewMockProver(t), }, route, ) @@ -68,7 +68,7 @@ func TestCheckSendFinalityProcess(t *testing.T) { sourceChainClient := mocks.NewMockClient(t) sourceChainClient.EXPECT().TxHeight(mock.Anything, mock.Anything).Return(uint64(100), nil).Once() - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Time{}, nil).Once() p, err := NewCheckSendFinality( @@ -88,7 +88,7 @@ func TestCheckSendFinalityProcess(t *testing.T) { sourceChainClient := mocks.NewMockClient(t) sourceChainClient.EXPECT().TxHeight(mock.Anything, mock.Anything).Return(uint64(150), nil).Once() - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Time{}, nil).Once() p, err := NewCheckSendFinality( diff --git a/link/internal/relay/processors/check_timeout_finality.go b/link/internal/relay/processors/check_timeout_finality.go index 13bb3677c..076449ee2 100644 --- a/link/internal/relay/processors/check_timeout_finality.go +++ b/link/internal/relay/processors/check_timeout_finality.go @@ -8,15 +8,15 @@ import ( "github.com/pkg/errors" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/store" + "github.com/cosmos/ibc/link/lightclient" ) // CheckTimeoutFinality gates timing out a packet on the source client's // proof generator currently being able to prove a destination-chain // timestamp past the timeout. type CheckTimeoutFinality struct { - proofGen proofgen.ProofGenerator + proofGen lightclient.Prover } func NewCheckTimeoutFinality(proofGenerators ProofGenerators, route Route) (CheckTimeoutFinality, error) { diff --git a/link/internal/relay/processors/check_timeout_finality_test.go b/link/internal/relay/processors/check_timeout_finality_test.go index 2bb674fe7..8e589cd17 100644 --- a/link/internal/relay/processors/check_timeout_finality_test.go +++ b/link/internal/relay/processors/check_timeout_finality_test.go @@ -27,7 +27,7 @@ func TestNewCheckTimeoutFinality(t *testing.T) { t.Run("resolvesProofGenerator", func(t *testing.T) { _, err := NewCheckTimeoutFinality( staticProofGenerators{ - proofgen.Key(route.SourceChainID, route.SourceClientID): mocks.NewMockProofGenerator(t), + proofgen.Key(route.SourceChainID, route.SourceClientID): mocks.NewMockProver(t), }, route, ) @@ -50,7 +50,7 @@ func TestCheckTimeoutFinalityProcess(t *testing.T) { } t.Run("timestampPastTimeoutIsFinalized", func(t *testing.T) { - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Unix(2000, 0), nil).Once() p, err := NewCheckTimeoutFinality( @@ -66,7 +66,7 @@ func TestCheckTimeoutFinalityProcess(t *testing.T) { }) t.Run("timestampBeforeTimeoutErrorsRetryable", func(t *testing.T) { - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Unix(500, 0), nil).Once() p, err := NewCheckTimeoutFinality( diff --git a/link/internal/relay/processors/check_write_ack_finality.go b/link/internal/relay/processors/check_write_ack_finality.go index 02002e9c9..e26b95b19 100644 --- a/link/internal/relay/processors/check_write_ack_finality.go +++ b/link/internal/relay/processors/check_write_ack_finality.go @@ -11,8 +11,8 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/store" + "github.com/cosmos/ibc/link/lightclient" ) // CheckWriteAckFinality gates ack relaying on the write ack tx's height @@ -20,7 +20,7 @@ import ( // currently prove. type CheckWriteAckFinality struct { destinationChainClient chains.Client - proofGen proofgen.ProofGenerator + proofGen lightclient.Prover } func NewCheckWriteAckFinality( diff --git a/link/internal/relay/processors/check_write_ack_finality_test.go b/link/internal/relay/processors/check_write_ack_finality_test.go index 986d159f7..38cc81827 100644 --- a/link/internal/relay/processors/check_write_ack_finality_test.go +++ b/link/internal/relay/processors/check_write_ack_finality_test.go @@ -23,7 +23,7 @@ func TestNewCheckWriteAckFinality(t *testing.T) { _, err := NewCheckWriteAckFinality( staticChains{}, staticProofGenerators{ - proofgen.Key(route.SourceChainID, route.SourceClientID): mocks.NewMockProofGenerator(t), + proofgen.Key(route.SourceChainID, route.SourceClientID): mocks.NewMockProver(t), }, route, ) @@ -60,7 +60,7 @@ func TestCheckWriteAckFinalityProcess(t *testing.T) { destinationChainClient := mocks.NewMockClient(t) destinationChainClient.EXPECT().TxHeight(mock.Anything, mock.Anything).Return(uint64(100), nil).Once() - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Time{}, nil).Once() p, err := NewCheckWriteAckFinality( @@ -80,7 +80,7 @@ func TestCheckWriteAckFinalityProcess(t *testing.T) { destinationChainClient := mocks.NewMockClient(t) destinationChainClient.EXPECT().TxHeight(mock.Anything, mock.Anything).Return(uint64(150), nil).Once() - proofGen := mocks.NewMockProofGenerator(t) + proofGen := mocks.NewMockProver(t) proofGen.EXPECT().LatestProvableHeight(mock.Anything).Return(uint64(100), time.Time{}, nil).Once() p, err := NewCheckWriteAckFinality( diff --git a/link/internal/relay/processors/processors.go b/link/internal/relay/processors/processors.go index 32eaf3b4e..c6c0086b7 100644 --- a/link/internal/relay/processors/processors.go +++ b/link/internal/relay/processors/processors.go @@ -7,9 +7,9 @@ import ( "time" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/relay/txbuilder" "github.com/cosmos/ibc/link/internal/store" + "github.com/cosmos/ibc/link/lightclient" ) // waitForChainTimeout bounds how long batch delivery waits for the target @@ -28,7 +28,7 @@ type ChainClients interface { // ProofGenerators resolves proof generators by (chainIDclientID). type ProofGenerators interface { - Get(chainID, clientID string) (proofgen.ProofGenerator, bool) + Get(chainID, clientID string) (lightclient.Prover, bool) } // TxBuilders resolves tx builders by chain id. diff --git a/link/internal/relay/processors/timeout_packet.go b/link/internal/relay/processors/timeout_packet.go index 4bd3904ce..e8aa3bb97 100644 --- a/link/internal/relay/processors/timeout_packet.go +++ b/link/internal/relay/processors/timeout_packet.go @@ -10,11 +10,11 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/relay/txbuilder" "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/txsubmitter" v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) // BatchTimeoutPacket delivers one timeout tx on the source chain for a batch @@ -22,7 +22,7 @@ import ( type BatchTimeoutPacket struct { sourceChainClient chains.Client route Route - proofGen proofgen.ProofGenerator + proofGen lightclient.Prover txBuilder txbuilder.TxBuilder txSubmitter txsubmitter.TxSubmitter storage TxStorage diff --git a/link/internal/relay/proofgen/attestation/generator.go b/link/internal/relay/proofgen/attestation/generator.go index 3604d3932..b52bc5c32 100644 --- a/link/internal/relay/proofgen/attestation/generator.go +++ b/link/internal/relay/proofgen/attestation/generator.go @@ -13,12 +13,10 @@ import ( "github.com/cosmos/ibc/link/attestor/evm/ibc" "github.com/cosmos/ibc/link/internal/chains" "github.com/cosmos/ibc/link/internal/service/attestor" - v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) -// Generator implements proofgen.ProofGenerator for one configured -// attestation light client: LatestProvableHeight/StateProof/PacketProofs all -// query the same fixed attestor set with the same quorum threshold +// Generator proves an attestation light client's state and packets. type Generator struct { attestors []attestor.Attestor threshold int @@ -63,7 +61,7 @@ func (g *Generator) StateProof(ctx context.Context, height uint64) ([]byte, erro func (g *Generator) PacketProofs( ctx context.Context, height uint64, - kind v2.ProofKind, + kind lightclient.ProofKind, packets []channeltypesv2.Packet, ) ([][]byte, error) { commitmentType, err := commitmentTypeOf(kind) @@ -124,13 +122,13 @@ func (g *Generator) PacketProofs( return proofs, nil } -func commitmentTypeOf(kind v2.ProofKind) (attestor.CommitmentType, error) { +func commitmentTypeOf(kind lightclient.ProofKind) (attestor.CommitmentType, error) { switch kind { - case v2.ProofKindPacketCommitment: + case lightclient.ProofKindPacketCommitment: return attestor.CommitmentTypePacket, nil - case v2.ProofKindAcknowledgement: + case lightclient.ProofKindAcknowledgement: return attestor.CommitmentTypeAck, nil - case v2.ProofKindReceiptAbsence: + case lightclient.ProofKindReceiptAbsence: return attestor.CommitmentTypeReceipt, nil default: return 0, errors.Errorf("unsupported proof kind %v", kind) diff --git a/link/internal/relay/proofgen/attestation/generator_test.go b/link/internal/relay/proofgen/attestation/generator_test.go index 0f9972f6e..6791d30fd 100644 --- a/link/internal/relay/proofgen/attestation/generator_test.go +++ b/link/internal/relay/proofgen/attestation/generator_test.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/ibc/link/internal/service/attestor" "github.com/cosmos/ibc/link/internal/tests/mocks" v2 "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" ) // signedStateAttestor builds a attestor.MockAttestor that answers @@ -122,7 +123,7 @@ func TestGeneratorPacketProofs(t *testing.T) { gen := New(attestors, 2, nil) - proofs, err := gen.PacketProofs(ctx, 20, v2.ProofKindPacketCommitment, packets) + proofs, err := gen.PacketProofs(ctx, 20, lightclient.ProofKindPacketCommitment, packets) require.NoError(t, err) require.Len(t, proofs, len(packets)) require.Equal(t, proofs[0], proofs[1], "the shared attestation blob is duplicated across every packet index") @@ -133,7 +134,7 @@ func TestGeneratorPacketProofs(t *testing.T) { // attestor, so the generator here is given no attestors at all. gen := New(nil, 2, nil) - _, err := gen.PacketProofs(ctx, 20, v2.ProofKindUnknown, packets) + _, err := gen.PacketProofs(ctx, 20, lightclient.ProofKindUnknown, packets) require.Error(t, err) }) } diff --git a/link/internal/relay/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index 9f7d59489..c68019b6c 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -1,85 +1,59 @@ // SPDX-License-Identifier: Apache-2.0 -// Package proofgen generates packet membership/non-membership proofs and -// light-client state proofs. There is one implementation per light-client -// type. +// Package proofgen resolves configured light-client provers. package proofgen import ( "context" - "time" "github.com/pkg/errors" - channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" "github.com/cosmos/ibc/link/internal/chains" "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/proofgen/attestation" - "github.com/cosmos/ibc/link/internal/service/attestor" - v2 "github.com/cosmos/ibc/link/internal/types/v2" + attestorservice "github.com/cosmos/ibc/link/internal/service/attestor" + "github.com/cosmos/ibc/link/lightclient" ) -// ProofGenerator generates packet membership/non-membership proofs and state -// proofs for one configured light client. -type ProofGenerator interface { - // LatestProvableHeight resolves the highest height a subsequent StateProof - // and PacketProofs call sharing that height can currently succeed at, - // along with that height's counterparty-chain timestamp - LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) - - // StateProof proves the light client's counterparty state at height. - StateProof(ctx context.Context, height uint64) ([]byte, error) - - // PacketProofs proves each packet's membership or non-membership at - // height, one proof per packet with indices aligned to packets. Returns - // an error if a proof cannot be generated for any packet - PacketProofs( - ctx context.Context, - height uint64, - kind v2.ProofKind, - packets []channeltypesv2.Packet, - ) ([][]byte, error) -} - -var _ ProofGenerator = (*attestation.Generator)(nil) - -// Key identifies one configured light client by the chain it lives on and -// its client id, the composite key ProofGenerator instances are scoped by. +// Key identifies a client on a chain. func Key(chainID, clientID string) string { return chainID + "/" + clientID } -// Set resolves a ProofGenerator by (chainID, clientID). +// Set resolves provers by chain and client ID. type Set struct { - generators map[string]ProofGenerator + generators map[string]lightclient.Prover } -func NewSet(generators map[string]ProofGenerator) *Set { +func NewSet(generators map[string]lightclient.Prover) *Set { if generators == nil { - generators = make(map[string]ProofGenerator) + generators = make(map[string]lightclient.Prover) } return &Set{generators: generators} } -func (s *Set) Get(chainID, clientID string) (ProofGenerator, bool) { +func (s *Set) Get(chainID, clientID string) (lightclient.Prover, bool) { generator, ok := s.generators[Key(chainID, clientID)] return generator, ok } -// NewSetFromConfig resolves a ProofGenerator for every client end of every -// configured connection, matching against attestors (this process's own -// local attestors plus every resolved remote one). +// NewSetFromConfig resolves provers for all configured client ends. func NewSetFromConfig( ctx context.Context, cfg config.Config, clientSet *chains.ClientSet, - attestors []attestor.Attestor, + attestors []attestorservice.Attestor, + reg *lightclient.Registry, ) (*Set, error) { - generators := make(map[string]ProofGenerator, len(cfg.Relayer.Connections)*2) + generators := make(map[string]lightclient.Prover, len(cfg.Relayer.Connections)*2) + chainInfos := make(map[string]lightclient.ChainInfo, len(cfg.Chains)) + for _, chain := range cfg.Chains { + chainInfos[chain.ChainID] = toChainInfo(chain) + } err := forEachClientEnd(cfg, func(connAlias string, self, counterparty config.ClientEnd) error { - return addGenerator(ctx, generators, connAlias, self, counterparty, clientSet, attestors) + return addGenerator(ctx, generators, connAlias, self, counterparty, chainInfos, clientSet, attestors, reg) }) if err != nil { return nil, err @@ -88,8 +62,7 @@ func NewSetFromConfig( return NewSet(generators), nil } -// forEachClientEnd calls fn once per client end of every configured -// connection, in both directions. +// forEachClientEnd calls fn for both ends of every connection. func forEachClientEnd(cfg config.Config, fn func(connAlias string, self, counterparty config.ClientEnd) error) error { for _, conn := range cfg.Relayer.Connections { for _, end := range []struct { @@ -109,15 +82,18 @@ func forEachClientEnd(cfg config.Config, fn func(connAlias string, self, counter func addGenerator( ctx context.Context, - generators map[string]ProofGenerator, + generators map[string]lightclient.Prover, connAlias string, client, clientCounterparty config.ClientEnd, + chainInfo map[string]lightclient.ChainInfo, clientSet *chains.ClientSet, - attestors []attestor.Attestor, + attestors []attestorservice.Attestor, + reg *lightclient.Registry, ) error { - switch client.Type { - case config.ClientTypeAttestation: - gen, err := attestation.ResolveGenerator(ctx, client, clientCounterparty, clientSet, attestors) + if client.Type == config.ClientTypeAttestation { + gen, err := attestation.ResolveGenerator( + ctx, client, clientCounterparty, clientSet, attestors, + ) if err != nil { return err } @@ -125,7 +101,60 @@ func addGenerator( generators[Key(client.ChainID, client.ClientID)] = gen return nil - default: - return errors.Errorf("connection %q: unsupported client type %q for proof generation", connAlias, client.Type) + } + + factory, ok := reg.Get(string(client.Type)) + if !ok { + registered := append([]string{string(config.ClientTypeAttestation)}, reg.Types()...) + return errors.Errorf( + "connection %q: no prover registered for client type %q (registered: %v)", + connAlias, client.Type, registered, + ) + } + hostChain, ok := chainInfo[client.ChainID] + if !ok { + return errors.Errorf("connection %q: no chain config for host chain %q", connAlias, client.ChainID) + } + counterpartyChain, ok := chainInfo[clientCounterparty.ChainID] + if !ok { + return errors.Errorf( + "connection %q: no chain config for counterparty chain %q", connAlias, clientCounterparty.ChainID, + ) + } + + gen, err := factory.New( + ctx, + lightclient.ProverFactoryOptions{ + Client: toClientInfo(client), + HostChain: hostChain, + CounterpartyChain: counterpartyChain, + }, + ) + if err != nil { + return errors.Wrapf(err, "connection %q", connAlias) + } + + generators[Key(client.ChainID, client.ClientID)] = gen + + return nil +} + +func toChainInfo(chain config.ChainConfig) lightclient.ChainInfo { + info := lightclient.ChainInfo{ChainID: chain.ChainID} + if chain.EVM != nil { + info.EVM = &lightclient.EVMChainInfo{ + RPC: chain.EVM.RPC, + ICS26Router: chain.EVM.ICS26Router, + } + } + + return info +} + +func toClientInfo(end config.ClientEnd) lightclient.ClientInfo { + return lightclient.ClientInfo{ + ClientID: end.ClientID, + Type: string(end.Type), + ClientParams: end.ClientParams, } } diff --git a/link/internal/relay/proofgen/proofgen_test.go b/link/internal/relay/proofgen/proofgen_test.go index 963af37fe..b39d41dda 100644 --- a/link/internal/relay/proofgen/proofgen_test.go +++ b/link/internal/relay/proofgen/proofgen_test.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/attestor" "github.com/cosmos/ibc/link/internal/tests/mocks" + "github.com/cosmos/ibc/link/lightclient" ) func testConnection() config.ConnectionConfig { @@ -80,12 +81,11 @@ func TestNewSetFromConfig(t *testing.T) { ctx := context.Background() t.Run("resolvesBothDirections", func(t *testing.T) { - // proves forEachClientEnd/addGenerator wiring: both the connection's - // client ends land in the returned Set under their own key. + // Both client ends must resolve under their own key. cfg, clientSet, attestors := testConfig(t) conn := cfg.Relayer.Connections[0] - set, err := NewSetFromConfig(ctx, cfg, clientSet, attestors) + set, err := NewSetFromConfig(ctx, cfg, clientSet, attestors, nil) require.NoError(t, err) _, ok := set.Get(conn.ClientA.ChainID, conn.ClientA.ClientID) @@ -106,12 +106,73 @@ func TestNewSetFromConfig(t *testing.T) { conn.ClientB.ChainID: mocks.NewMockClient(t), }) - cfg := config.Config{Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}} + cfg := config.Config{ + Chains: []config.ChainConfig{ + {ChainID: conn.ClientA.ChainID, EVM: &config.EVMChainConfig{RPC: "http://chain-a", ICS26Router: "0xa"}}, + {ChainID: conn.ClientB.ChainID, EVM: &config.EVMChainConfig{RPC: "http://chain-b", ICS26Router: "0xb"}}, + }, + Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}, + } // ACT - _, err := NewSetFromConfig(ctx, cfg, clientSet, nil) + _, err := NewSetFromConfig(ctx, cfg, clientSet, nil, nil) // ASSERT - require.ErrorContains(t, err, `unsupported client type "tendermint"`) + require.ErrorContains(t, err, `no prover registered for client type "tendermint"`) }) + + t.Run("arbitraryRegisteredClientTypeResolves", func(t *testing.T) { + // the registry, not a hardcoded switch, decides what is relayable: + // a client type with no attestors and no built-in support resolves + // purely because a factory was registered for it. + conn := testConnection() + conn.ClientA.Type = "myclient" + conn.ClientB.Type = "myclient" + + clientSet := chains.NewClientSet(map[string]chains.Client{ + conn.ClientA.ChainID: mocks.NewMockClient(t), + conn.ClientB.ChainID: mocks.NewMockClient(t), + }) + + cfg := config.Config{ + Chains: []config.ChainConfig{ + {ChainID: conn.ClientA.ChainID, EVM: &config.EVMChainConfig{RPC: "http://chain-a", ICS26Router: "0xa"}}, + {ChainID: conn.ClientB.ChainID, EVM: &config.EVMChainConfig{RPC: "http://chain-b", ICS26Router: "0xb"}}, + }, + Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}, + } + + reg := lightclient.NewRegistry() + built := make(chan lightclient.ProverFactoryOptions, 2) + require.NoError(t, reg.Register(stubFactory{built: built})) + + set, err := NewSetFromConfig(ctx, cfg, clientSet, nil, reg) + require.NoError(t, err) + first := <-built + require.Equal(t, conn.ClientA.ChainID, first.HostChain.ChainID) + require.Equal(t, "http://chain-a", first.HostChain.EVM.RPC) + require.Equal(t, conn.ClientB.ChainID, first.CounterpartyChain.ChainID) + require.Equal(t, "http://chain-b", first.CounterpartyChain.EVM.RPC) + + _, ok := set.Get(conn.ClientA.ChainID, conn.ClientA.ClientID) + require.True(t, ok) + }) +} + +// stubFactory is a light client type that exists only in this test. +type stubFactory struct { + built chan<- lightclient.ProverFactoryOptions } + +func (stubFactory) Type() string { return "myclient" } + +func (f stubFactory) New( + _ context.Context, options lightclient.ProverFactoryOptions, +) (lightclient.Prover, error) { + if f.built != nil { + f.built <- options + } + return stubProver{}, nil +} + +type stubProver struct{ lightclient.Prover } diff --git a/link/internal/relay/txbuilder/evm/evm.go b/link/internal/relay/txbuilder/evm/evm.go index 062a6e8f6..3a7fe9472 100644 --- a/link/internal/relay/txbuilder/evm/evm.go +++ b/link/internal/relay/txbuilder/evm/evm.go @@ -123,7 +123,7 @@ func height(h uint64) ics26router.IICS02ClientMsgsHeight { } // packUpdateClient packs a call to updateClient(clientId, updateMsg), where -// updateMsg is the already-encoded proof produced by proofgen.ProofGenerator.StateProof. +// updateMsg is an encoded light-client state proof. func packUpdateClient(clientID string, updateMsg []byte) ([]byte, error) { packed, err := calldata(func(opts *bind.TransactOpts) (*types.Transaction, error) { return router.UpdateClient(opts, clientID, updateMsg) diff --git a/link/internal/tests/mocks/proofgen.go b/link/internal/tests/mocks/proofgen.go index 51e9fe6b0..3ab1ba109 100644 --- a/link/internal/tests/mocks/proofgen.go +++ b/link/internal/tests/mocks/proofgen.go @@ -7,18 +7,18 @@ package mocks import ( "context" "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" - "github.com/cosmos/ibc/link/internal/types/v2" + "github.com/cosmos/ibc/link/lightclient" mock "github.com/stretchr/testify/mock" "time" ) -// NewMockProofGenerator creates a new instance of MockProofGenerator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockProver creates a new instance of MockProver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockProofGenerator(t interface { +func NewMockProver(t interface { mock.TestingT Cleanup(func()) -}) *MockProofGenerator { - mock := &MockProofGenerator{} +}) *MockProver { + mock := &MockProver{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) @@ -26,21 +26,21 @@ func NewMockProofGenerator(t interface { return mock } -// MockProofGenerator is an autogenerated mock type for the ProofGenerator type -type MockProofGenerator struct { +// MockProver is an autogenerated mock type for the Prover type +type MockProver struct { mock.Mock } -type MockProofGenerator_Expecter struct { +type MockProver_Expecter struct { mock *mock.Mock } -func (_m *MockProofGenerator) EXPECT() *MockProofGenerator_Expecter { - return &MockProofGenerator_Expecter{mock: &_m.Mock} +func (_m *MockProver) EXPECT() *MockProver_Expecter { + return &MockProver_Expecter{mock: &_m.Mock} } -// LatestProvableHeight provides a mock function for the type MockProofGenerator -func (_mock *MockProofGenerator) LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) { +// LatestProvableHeight provides a mock function for the type MockProver +func (_mock *MockProver) LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) { ret := _mock.Called(ctx) if len(ret) == 0 { @@ -71,18 +71,18 @@ func (_mock *MockProofGenerator) LatestProvableHeight(ctx context.Context) (uint return r0, r1, r2 } -// MockProofGenerator_LatestProvableHeight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestProvableHeight' -type MockProofGenerator_LatestProvableHeight_Call struct { +// MockProver_LatestProvableHeight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestProvableHeight' +type MockProver_LatestProvableHeight_Call struct { *mock.Call } // LatestProvableHeight is a helper method to define mock.On call // - ctx context.Context -func (_e *MockProofGenerator_Expecter) LatestProvableHeight(ctx any) *MockProofGenerator_LatestProvableHeight_Call { - return &MockProofGenerator_LatestProvableHeight_Call{Call: _e.mock.On("LatestProvableHeight", ctx)} +func (_e *MockProver_Expecter) LatestProvableHeight(ctx any) *MockProver_LatestProvableHeight_Call { + return &MockProver_LatestProvableHeight_Call{Call: _e.mock.On("LatestProvableHeight", ctx)} } -func (_c *MockProofGenerator_LatestProvableHeight_Call) Run(run func(ctx context.Context)) *MockProofGenerator_LatestProvableHeight_Call { +func (_c *MockProver_LatestProvableHeight_Call) Run(run func(ctx context.Context)) *MockProver_LatestProvableHeight_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -95,18 +95,18 @@ func (_c *MockProofGenerator_LatestProvableHeight_Call) Run(run func(ctx context return _c } -func (_c *MockProofGenerator_LatestProvableHeight_Call) Return(v uint64, time1 time.Time, err error) *MockProofGenerator_LatestProvableHeight_Call { +func (_c *MockProver_LatestProvableHeight_Call) Return(v uint64, time1 time.Time, err error) *MockProver_LatestProvableHeight_Call { _c.Call.Return(v, time1, err) return _c } -func (_c *MockProofGenerator_LatestProvableHeight_Call) RunAndReturn(run func(ctx context.Context) (uint64, time.Time, error)) *MockProofGenerator_LatestProvableHeight_Call { +func (_c *MockProver_LatestProvableHeight_Call) RunAndReturn(run func(ctx context.Context) (uint64, time.Time, error)) *MockProver_LatestProvableHeight_Call { _c.Call.Return(run) return _c } -// PacketProofs provides a mock function for the type MockProofGenerator -func (_mock *MockProofGenerator) PacketProofs(ctx context.Context, height uint64, kind v2.ProofKind, packets []types.Packet) ([][]byte, error) { +// PacketProofs provides a mock function for the type MockProver +func (_mock *MockProver) PacketProofs(ctx context.Context, height uint64, kind lightclient.ProofKind, packets []types.Packet) ([][]byte, error) { ret := _mock.Called(ctx, height, kind, packets) if len(ret) == 0 { @@ -115,17 +115,17 @@ func (_mock *MockProofGenerator) PacketProofs(ctx context.Context, height uint64 var r0 [][]byte var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, v2.ProofKind, []types.Packet) ([][]byte, error)); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, lightclient.ProofKind, []types.Packet) ([][]byte, error)); ok { return returnFunc(ctx, height, kind, packets) } - if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, v2.ProofKind, []types.Packet) [][]byte); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, lightclient.ProofKind, []types.Packet) [][]byte); ok { r0 = returnFunc(ctx, height, kind, packets) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([][]byte) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, uint64, v2.ProofKind, []types.Packet) error); ok { + if returnFunc, ok := ret.Get(1).(func(context.Context, uint64, lightclient.ProofKind, []types.Packet) error); ok { r1 = returnFunc(ctx, height, kind, packets) } else { r1 = ret.Error(1) @@ -133,21 +133,21 @@ func (_mock *MockProofGenerator) PacketProofs(ctx context.Context, height uint64 return r0, r1 } -// MockProofGenerator_PacketProofs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PacketProofs' -type MockProofGenerator_PacketProofs_Call struct { +// MockProver_PacketProofs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PacketProofs' +type MockProver_PacketProofs_Call struct { *mock.Call } // PacketProofs is a helper method to define mock.On call // - ctx context.Context // - height uint64 -// - kind v2.ProofKind +// - kind lightclient.ProofKind // - packets []types.Packet -func (_e *MockProofGenerator_Expecter) PacketProofs(ctx any, height any, kind any, packets any) *MockProofGenerator_PacketProofs_Call { - return &MockProofGenerator_PacketProofs_Call{Call: _e.mock.On("PacketProofs", ctx, height, kind, packets)} +func (_e *MockProver_Expecter) PacketProofs(ctx any, height any, kind any, packets any) *MockProver_PacketProofs_Call { + return &MockProver_PacketProofs_Call{Call: _e.mock.On("PacketProofs", ctx, height, kind, packets)} } -func (_c *MockProofGenerator_PacketProofs_Call) Run(run func(ctx context.Context, height uint64, kind v2.ProofKind, packets []types.Packet)) *MockProofGenerator_PacketProofs_Call { +func (_c *MockProver_PacketProofs_Call) Run(run func(ctx context.Context, height uint64, kind lightclient.ProofKind, packets []types.Packet)) *MockProver_PacketProofs_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -157,9 +157,9 @@ func (_c *MockProofGenerator_PacketProofs_Call) Run(run func(ctx context.Context if args[1] != nil { arg1 = args[1].(uint64) } - var arg2 v2.ProofKind + var arg2 lightclient.ProofKind if args[2] != nil { - arg2 = args[2].(v2.ProofKind) + arg2 = args[2].(lightclient.ProofKind) } var arg3 []types.Packet if args[3] != nil { @@ -175,18 +175,18 @@ func (_c *MockProofGenerator_PacketProofs_Call) Run(run func(ctx context.Context return _c } -func (_c *MockProofGenerator_PacketProofs_Call) Return(bytess [][]byte, err error) *MockProofGenerator_PacketProofs_Call { +func (_c *MockProver_PacketProofs_Call) Return(bytess [][]byte, err error) *MockProver_PacketProofs_Call { _c.Call.Return(bytess, err) return _c } -func (_c *MockProofGenerator_PacketProofs_Call) RunAndReturn(run func(ctx context.Context, height uint64, kind v2.ProofKind, packets []types.Packet) ([][]byte, error)) *MockProofGenerator_PacketProofs_Call { +func (_c *MockProver_PacketProofs_Call) RunAndReturn(run func(ctx context.Context, height uint64, kind lightclient.ProofKind, packets []types.Packet) ([][]byte, error)) *MockProver_PacketProofs_Call { _c.Call.Return(run) return _c } -// StateProof provides a mock function for the type MockProofGenerator -func (_mock *MockProofGenerator) StateProof(ctx context.Context, height uint64) ([]byte, error) { +// StateProof provides a mock function for the type MockProver +func (_mock *MockProver) StateProof(ctx context.Context, height uint64) ([]byte, error) { ret := _mock.Called(ctx, height) if len(ret) == 0 { @@ -213,19 +213,19 @@ func (_mock *MockProofGenerator) StateProof(ctx context.Context, height uint64) return r0, r1 } -// MockProofGenerator_StateProof_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StateProof' -type MockProofGenerator_StateProof_Call struct { +// MockProver_StateProof_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StateProof' +type MockProver_StateProof_Call struct { *mock.Call } // StateProof is a helper method to define mock.On call // - ctx context.Context // - height uint64 -func (_e *MockProofGenerator_Expecter) StateProof(ctx any, height any) *MockProofGenerator_StateProof_Call { - return &MockProofGenerator_StateProof_Call{Call: _e.mock.On("StateProof", ctx, height)} +func (_e *MockProver_Expecter) StateProof(ctx any, height any) *MockProver_StateProof_Call { + return &MockProver_StateProof_Call{Call: _e.mock.On("StateProof", ctx, height)} } -func (_c *MockProofGenerator_StateProof_Call) Run(run func(ctx context.Context, height uint64)) *MockProofGenerator_StateProof_Call { +func (_c *MockProver_StateProof_Call) Run(run func(ctx context.Context, height uint64)) *MockProver_StateProof_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -243,12 +243,12 @@ func (_c *MockProofGenerator_StateProof_Call) Run(run func(ctx context.Context, return _c } -func (_c *MockProofGenerator_StateProof_Call) Return(bytes []byte, err error) *MockProofGenerator_StateProof_Call { +func (_c *MockProver_StateProof_Call) Return(bytes []byte, err error) *MockProver_StateProof_Call { _c.Call.Return(bytes, err) return _c } -func (_c *MockProofGenerator_StateProof_Call) RunAndReturn(run func(ctx context.Context, height uint64) ([]byte, error)) *MockProofGenerator_StateProof_Call { +func (_c *MockProver_StateProof_Call) RunAndReturn(run func(ctx context.Context, height uint64) ([]byte, error)) *MockProver_StateProof_Call { _c.Call.Return(run) return _c } diff --git a/link/internal/types/v2/types.go b/link/internal/types/v2/types.go index faba22dd7..4925e6db4 100644 --- a/link/internal/types/v2/types.go +++ b/link/internal/types/v2/types.go @@ -29,17 +29,6 @@ const ( WriteAckStatusError ) -// ProofKind the kind of packet claim a proof attests to. -type ProofKind int - -// Proof kinds -const ( - ProofKindUnknown ProofKind = iota - ProofKindPacketCommitment - ProofKindAcknowledgement - ProofKindReceiptAbsence -) - // RelayKind the packet operation one PacketRelayItem asks to perform. type RelayKind int diff --git a/link/lightclient/params.go b/link/lightclient/params.go new file mode 100644 index 000000000..528c30536 --- /dev/null +++ b/link/lightclient/params.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 + +package lightclient + +import ( + "github.com/goccy/go-yaml" + "github.com/pkg/errors" +) + +// RawParams stores client-specific configuration for a ProverFactory. +type RawParams struct { + raw []byte +} + +// NewRawParams wraps an encoded YAML document. +func NewRawParams(raw []byte) *RawParams { + return &RawParams{raw: append([]byte(nil), raw...)} +} + +// UnmarshalYAML captures the params block without interpreting it. +func (p *RawParams) UnmarshalYAML(b []byte) error { + if p == nil { + return errors.New("lightclient: UnmarshalYAML on nil RawParams") + } + + p.raw = append([]byte(nil), b...) + + return nil +} + +// MarshalYAML writes the captured block back out unchanged. +func (p RawParams) MarshalYAML() ([]byte, error) { + return p.raw, nil +} + +// IsEmpty reports whether params were configured. +func (p *RawParams) IsEmpty() bool { + return p == nil || len(p.raw) == 0 +} + +// Decode strictly unmarshals params into v. Empty params leave v unchanged. +func (p *RawParams) Decode(v any) error { + if p.IsEmpty() { + return nil + } + + if err := yaml.UnmarshalWithOptions(p.raw, v, yaml.DisallowUnknownField()); err != nil { + return errors.Wrap(err, "decoding params") + } + + return nil +} + +// Bytes returns the captured document. +func (p *RawParams) Bytes() []byte { + if p == nil { + return nil + } + + return p.raw +} diff --git a/link/lightclient/params_test.go b/link/lightclient/params_test.go new file mode 100644 index 000000000..9edb48c00 --- /dev/null +++ b/link/lightclient/params_test.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +package lightclient + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type stubParams struct { + ProverURL string `yaml:"proverUrl"` +} + +// A misspelled params key must fail rather than silently becoming a zero +// value: the top-level decode's DisallowUnknownField cannot see inside a +// captured params block, so RawParams.Decode has to re-apply it. +func TestRawParamsRejectsUnknownField(t *testing.T) { + params := NewRawParams([]byte("proverURL: https://example.com\n")) + + var p stubParams + require.ErrorContains(t, params.Decode(&p), "proverURL") + require.Empty(t, p.ProverURL) +} + +func TestRawParamsRoundTrip(t *testing.T) { + params := NewRawParams([]byte("proverUrl: https://example.com\n")) + + var p stubParams + require.NoError(t, params.Decode(&p)) + require.Equal(t, "https://example.com", p.ProverURL) +} + +func TestRawParamsEmptyDecodeIsNoOp(t *testing.T) { + var params *RawParams + + var p stubParams + require.NoError(t, params.Decode(&p)) + require.True(t, params.IsEmpty()) +} diff --git a/link/lightclient/proofgen.go b/link/lightclient/proofgen.go new file mode 100644 index 000000000..b14a2d680 --- /dev/null +++ b/link/lightclient/proofgen.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package lightclient defines custom light-client proof generation. +package lightclient + +import ( + "context" + "time" + + channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" +) + +// Prover generates proofs for one light client. +type Prover interface { + // LatestProvableHeight returns the latest provable height and timestamp. + LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) + + // StateProof proves counterparty state at height. + StateProof(ctx context.Context, height uint64) ([]byte, error) + + // PacketProofs returns one proof per packet, in packet order. + PacketProofs( + ctx context.Context, + height uint64, + kind ProofKind, + packets []channeltypesv2.Packet, + ) ([][]byte, error) +} + +// ProofKind identifies the packet claim being proved. +type ProofKind int + +// Proof kinds. +const ( + ProofKindUnknown ProofKind = iota + ProofKindPacketCommitment + ProofKindAcknowledgement + ProofKindReceiptAbsence +) diff --git a/link/lightclient/registry.go b/link/lightclient/registry.go new file mode 100644 index 000000000..787b4d4ce --- /dev/null +++ b/link/lightclient/registry.go @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 + +package lightclient + +import ( + "context" + "sort" + + "github.com/pkg/errors" +) + +// ClientInfo describes a configured light client. +type ClientInfo struct { + ClientID string + Type string + ClientParams *RawParams +} + +// ChainInfo contains chain settings available to provers. +type ChainInfo struct { + ChainID string + EVM *EVMChainInfo +} + +// EVMChainInfo contains EVM chain settings. +type EVMChainInfo struct { + RPC string + ICS26Router string +} + +// ProverFactoryOptions contains inputs for constructing a prover. +type ProverFactoryOptions struct { + Client ClientInfo + HostChain ChainInfo + CounterpartyChain ChainInfo +} + +// ProverFactory builds provers for one custom light-client type. +type ProverFactory interface { + // Type returns the configured client type name. + Type() string + + // New builds a prover for Client. + New(ctx context.Context, options ProverFactoryOptions) (Prover, error) +} + +// Registry resolves custom light-client factories by type. +type Registry struct { + factories map[string]ProverFactory +} + +func NewRegistry() *Registry { + return &Registry{factories: make(map[string]ProverFactory)} +} + +// Register adds a factory under its type name. +func (r *Registry) Register(f ProverFactory) error { + if f == nil { + return errors.New("factory must not be nil") + } + clientType := f.Type() + switch clientType { + case "": + return errors.New("client type must not be empty") + case "attestation": + return errors.Errorf("client type %q is built in and cannot be overridden", clientType) + } + + if _, exists := r.factories[clientType]; exists { + return errors.Errorf("client type %q already registered", clientType) + } + + r.factories[clientType] = f + + return nil +} + +func (r *Registry) Get(clientType string) (ProverFactory, bool) { + if r == nil { + return nil, false + } + + f, ok := r.factories[clientType] + + return f, ok +} + +// Types returns the registered type names in sorted order. +func (r *Registry) Types() []string { + if r == nil { + return nil + } + + types := make([]string, 0, len(r.factories)) + for name := range r.factories { + types = append(types, name) + } + + sort.Strings(types) + + return types +} diff --git a/link/lightclient/registry_test.go b/link/lightclient/registry_test.go new file mode 100644 index 000000000..90a336d78 --- /dev/null +++ b/link/lightclient/registry_test.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +package lightclient + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +type testFactory string + +func (f testFactory) Type() string { return string(f) } + +func (testFactory) New(context.Context, ProverFactoryOptions) (Prover, error) { + return nil, nil +} + +func TestRegistryRegisterUsesFactoryType(t *testing.T) { + registry := NewRegistry() + factory := testFactory("custom") + require.NoError(t, registry.Register(factory)) + + got, ok := registry.Get("custom") + require.True(t, ok) + require.Equal(t, ProverFactory(factory), got) + require.ErrorContains(t, registry.Register(factory), "already registered") +} + +func TestRegistryRejectsBuiltInAttestation(t *testing.T) { + require.ErrorContains(t, NewRegistry().Register(testFactory("attestation")), "built in") +} diff --git a/link/lightclient/remotepoc/attestation_handler.go b/link/lightclient/remotepoc/attestation_handler.go new file mode 100644 index 000000000..9c3d9e069 --- /dev/null +++ b/link/lightclient/remotepoc/attestation_handler.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +package remotepoc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/pkg/errors" + + "github.com/cosmos/ibc/link/internal/chains" + "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/internal/relay/proofgen/attestation" + attestorservice "github.com/cosmos/ibc/link/internal/service/attestor" + "github.com/cosmos/ibc/link/internal/service/signer" + "github.com/cosmos/ibc/link/lightclient" +) + +// NewHandler serves a prover over HTTP. +func NewHandler(prover lightclient.Prover) *http.Server { + mux := http.NewServeMux() + mux.HandleFunc("POST /proof", func(w http.ResponseWriter, r *http.Request) { + var req request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + _ = json.NewEncoder(w).Encode(response{Error: err.Error()}) + return + } + + res := response{} + var err error + switch req.Operation { + case "latest": + res.Height, res.Timestamp, err = prover.LatestProvableHeight(r.Context()) + case "state": + res.Proof, err = prover.StateProof(r.Context(), req.Height) + case "packets": + var proofs [][]byte + proofs, err = prover.PacketProofs(r.Context(), req.Height, req.Kind, req.Packets) + res.Proofs = proofs + default: + err = fmt.Errorf("unknown operation %q", req.Operation) + } + if err != nil { + res = response{Error: err.Error()} + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(res) + }) + + return &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second} +} + +// NewAttestationHandler serves a configured attestation prover. +func NewAttestationHandler(ctx context.Context, configPath, chainID, clientID string) (*http.Server, error) { + cfg, err := config.LoadFromFile(configPath, true, true) + if err != nil { + return nil, errors.Wrap(err, "load config") + } + + clients, err := chains.NewClientSetFromConfig(cfg) + if err != nil { + return nil, errors.Wrap(err, "build chain clients") + } + signers, err := signer.NewSetFromConfig(ctx, cfg.Signers) + if err != nil { + return nil, errors.Wrap(err, "build signers") + } + local, remoteAttestors, err := attestorservice.ResolveFromConfig(ctx, cfg.Attestors, clients, signers) + if err != nil { + return nil, errors.Wrap(err, "resolve attestors") + } + + self, counterparty, ok := cfg.Relayer.ClientEnd(chainID, clientID) + if !ok { + return nil, errors.Errorf("client %q on chain %q is not configured", clientID, chainID) + } + prover, err := attestation.ResolveGenerator( + ctx, self, counterparty, clients, append(local, remoteAttestors...), + ) + if err != nil { + return nil, errors.Wrap(err, "resolve attestation prover") + } + + return NewHandler(prover), nil +} diff --git a/link/lightclient/remotepoc/remote.go b/link/lightclient/remotepoc/remote.go new file mode 100644 index 000000000..1d4e911e1 --- /dev/null +++ b/link/lightclient/remotepoc/remote.go @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package remotepoc provides an experimental HTTP prover. +package remotepoc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" + "github.com/cosmos/ibc/link/lightclient" +) + +// Type is the remote prover's config type. +const Type = "remote" + +// ClientParams configures the remote service. +type ClientParams struct { + URL string `yaml:"url"` +} + +// Factory constructs remote provers. +type Factory struct { + HTTPClient *http.Client +} + +func (Factory) Type() string { return Type } + +func (f Factory) New( + _ context.Context, + options lightclient.ProverFactoryOptions, +) (lightclient.Prover, error) { + var p ClientParams + if err := options.Client.ClientParams.Decode(&p); err != nil { + return nil, err + } + u, err := url.ParseRequestURI(p.URL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return nil, errors.New("url must be an absolute HTTP(S) URL") + } + + client := f.HTTPClient + if client == nil { + client = &http.Client{Timeout: 30 * time.Second} + } + + return &Prover{url: strings.TrimRight(p.URL, "/") + "/proof", client: client}, nil +} + +// Prover forwards proof generation to an HTTP service. +type Prover struct { + url string + client *http.Client +} + +type request struct { + Operation string `json:"operation"` + Height uint64 `json:"height,omitempty"` + Kind lightclient.ProofKind `json:"kind,omitempty"` + Packets []channeltypesv2.Packet `json:"packets,omitempty"` +} + +type response struct { + Height uint64 `json:"height,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` + Proof []byte `json:"proof,omitempty"` + Proofs [][]byte `json:"proofs,omitempty"` + Error string `json:"error,omitempty"` +} + +func (p *Prover) LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) { + res, err := p.call(ctx, request{Operation: "latest"}) + return res.Height, res.Timestamp, err +} + +func (p *Prover) StateProof(ctx context.Context, height uint64) ([]byte, error) { + res, err := p.call(ctx, request{Operation: "state", Height: height}) + return res.Proof, err +} + +func (p *Prover) PacketProofs( + ctx context.Context, + height uint64, + kind lightclient.ProofKind, + packets []channeltypesv2.Packet, +) ([][]byte, error) { + res, err := p.call(ctx, request{Operation: "packets", Height: height, Kind: kind, Packets: packets}) + return res.Proofs, err +} + +func (p *Prover) call(ctx context.Context, req request) (response, error) { + body, err := json.Marshal(req) + if err != nil { + return response{}, err + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, strings.NewReader(string(body))) + if err != nil { + return response{}, err + } + httpReq.Header.Set("Content-Type", "application/json") + + httpRes, err := p.client.Do(httpReq) + if err != nil { + return response{}, err + } + defer func() { _ = httpRes.Body.Close() }() + + var res response + if err := json.NewDecoder(httpRes.Body).Decode(&res); err != nil { + return response{}, fmt.Errorf("remote proof service: %w", err) + } + if res.Error != "" { + return response{}, errors.New(res.Error) + } + + return res, nil +}