From 51688332e801998de531a576de5833eb6ffb13fe Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Mon, 17 Aug 2026 11:52:12 -0700 Subject: [PATCH 01/17] expose a public proof generator registry for custom light clients Signed-off-by: Dennis Fang --- e2e/go.mod | 23 ++ e2e/go.sum | 70 +++- e2e/inprocess_proofgen_test.go | 301 ++++++++++++++++++ link/app/app.go | 168 ++++++++++ link/cmd/ibc/relayer.go | 4 +- link/internal/bootstrap/bootstrap.go | 25 +- link/internal/clienttypes/clienttypes.go | 42 +++ link/internal/config/clients.go | 56 ++++ link/internal/config/clients_test.go | 140 ++++++++ link/internal/config/relayer.go | 11 +- link/internal/config/relayer_test.go | 13 +- link/internal/livevalidate/quorum.go | 12 +- link/internal/livevalidate/quorum_test.go | 3 +- .../relay/proofgen/attestation/factory.go | 68 ++++ link/internal/relay/proofgen/proofgen.go | 86 ++--- link/internal/relay/proofgen/proofgen_test.go | 57 +++- link/internal/types/v2/client.go | 14 +- link/internal/types/v2/types.go | 15 +- link/proofgen/params.go | 77 +++++ link/proofgen/proofgen.go | 87 +++++ link/proofgen/registry.go | 99 ++++++ 21 files changed, 1302 insertions(+), 69 deletions(-) create mode 100644 e2e/inprocess_proofgen_test.go create mode 100644 link/app/app.go create mode 100644 link/internal/clienttypes/clienttypes.go create mode 100644 link/internal/config/clients.go create mode 100644 link/internal/config/clients_test.go create mode 100644 link/internal/relay/proofgen/attestation/factory.go create mode 100644 link/proofgen/params.go create mode 100644 link/proofgen/proofgen.go create mode 100644 link/proofgen/registry.go diff --git a/e2e/go.mod b/e2e/go.mod index 3c65165aa..be35c9e6e 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,9 @@ require ( ) replace github.com/cosmos/ibc/link => ../link + +// Mirrors link/go.mod. Replace directives are not inherited from dependency +// modules, so importing link/app — which compiles link's signer stack — makes +// this module resolve stock cometbft, which lacks privval.ParseNoiseAddr and +// breaks github.com/cosmos/kms/config. +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/inprocess_proofgen_test.go b/e2e/inprocess_proofgen_test.go new file mode 100644 index 000000000..6a3f87590 --- /dev/null +++ b/e2e/inprocess_proofgen_test.go @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: Apache-2.0 + +package e2e_test + +import ( + "context" + "fmt" + "math/big" + "net/http" + "os" + "path/filepath" + "strconv" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/ethereum/go-ethereum/crypto" + "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/app" + "github.com/cosmos/ibc/link/keyfile" + "github.com/cosmos/ibc/link/proofgen" +) + +// TestInProcessRelayerWithRegisteredProofGenerator is a proof of concept for +// the pluggable proof generator work. It proves three things the existing +// suite cannot: +// +// 1. A relayer can be constructed and run entirely in-process through the +// public link/app entrypoint, rather than by executing the ibc binary. +// The harness daemon shells out, so there is no way to hand it a registry. +// 2. Attestors run inside that same process (dual mode), with no separate +// attestor daemons. +// 3. A caller-registered light client type reaches proof generation. The +// attestation client is resolved through the same registry, so relaying +// working at all is evidence the switch-to-registry port is sound. +// +// It deliberately does not use e2etest.StartRelayer or AwaitState, both of +// which require the daemon handle. No harness files are modified. +func TestInProcessRelayerWithRegisteredProofGenerator(t *testing.T) { + t.Parallel() + + chainSpecs := e2etest.EVMChains(t, e2etest.EVMRequirements{}, e2etest.ChainA, e2etest.ChainB) + spec, runtime := attestedMesh(chainSpecs) + env := e2etest.Start(t, spec, runtime) + + sender := e2etest.NewSigner(t) + relayerSigner := e2etest.NewSigner(t) + route := e2etest.AtoB(e2etest.ChainA, e2etest.ChainB) + + // Capture the config the harness builds, rewriting its attestors from + // separate gRPC daemons to in-process local ones backed by the same keys + // the environment baked into each client's on-chain attestation set. + var captured ibclink.RelayerConfig + + keyDir := t.TempDir() + + driver, deployment := e2etest.DeployWithRelayerConfig( + t, + env, + sender, + relayerSigner, + func(config *ibclink.RelayerConfig) { + config.Attestors = localAttestors(t, env, runtime, keyDir) + captured = *config + }, + route, + ) + require.NotNil(t, driver) + + // The harness writes chain RPCs as ${IBC_LINK_CHAIN_RPC_...} placeholders + // and injects the values into the environment of the subprocess it spawns. + // Running in-process there is no such subprocess, so substitute the real + // endpoints. + for i := range captured.Chains { + captured.Chains[i].RPC = rpcURLForEVMChainID(t, env, captured.Chains[i].ChainID) + } + + // The in-process relayer gets its own config file and database so it never + // contends with the harness driver's. + runDir := t.TempDir() + captured.DBPath = filepath.Join(runDir, "inprocess.db") + configPath := filepath.Join(runDir, "ibc-link.config.yaml") + require.NoError(t, ibclink.WriteRelayerConfig(configPath, captured), "write in-process config") + + // A client type that exists only in this test, proving the registry + // accepts arbitrary names from outside the relayer's own packages. + registry := proofgen.NewRegistry() + require.NoError(t, registry.Register("poc-custom-client", pocFactory{})) + + relayer, err := app.New(t.Context(), app.Options{ + ConfigPath: configPath, + Registry: registry, + }) + require.NoError(t, err, "construct relayer in-process") + + address, err := relayer.Start(t.Context()) + require.NoError(t, err, "start relayer") + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + require.NoError(t, relayer.Stop(ctx), "stop relayer") + }) + + require.ElementsMatch(t, evmChainIDs(t, env), relayer.ChainIDs(), "relayer connected to both chains") + + client := relayerv2.NewRelayerApiServiceClient( + &http.Client{Timeout: 10 * time.Second}, + "http://"+address.String(), + connect.WithGRPC(), + ) + + ctx := t.Context() + + iftApp := e2etest.NewIFT(t, env, deployment, sender, route) + transfer, err := iftApp.Send(ctx, e2etest.IFTRequest{Amount: big.NewInt(1_234_000)}) + require.NoError(t, err, "send transfer") + + packet := transfer.PacketTx() + sourceChainID := evmChainID(t, env, packet.Source) + + _, err = client.Relay(ctx, connect.NewRequest(&relayerv2.RelayRequest{ + TxHash: packet.SourceTxHash, + SourceChainId: sourceChainID, + Selection: &relayerv2.RelayRequest_AllPackets{AllPackets: &relayerv2.AllPackets{}}, + })) + require.NoError(t, err, "trigger relay") + + status := awaitSucceeded(ctx, t, client, packet.SourceTxHash, sourceChainID) + require.NotEmpty(t, status.GetRecvTx().GetTxHash(), "recv tx recorded") + require.NotEmpty(t, status.GetAckTx().GetTxHash(), "ack tx recorded") + + require.NoError(t, transfer.VerifyDelivered(ctx)) + require.NoError(t, transfer.VerifyBurned(ctx), "a successful ack must not also refund") +} + +// awaitSucceeded polls the relayer's own status API until the packet reports +// success. A local reimplementation of e2etest.AwaitState, which needs the +// daemon handle this test does not have. +func awaitSucceeded( + ctx context.Context, + t *testing.T, + client relayerv2.RelayerApiServiceClient, + txHash, sourceChainID string, +) *relayerv2.PacketStatus { + t.Helper() + + deadline := time.Now().Add(90 * time.Second) + + var last relayerv2.PacketState + + for time.Now().Before(deadline) { + res, err := client.Status(ctx, connect.NewRequest(&relayerv2.StatusRequest{ + TxHash: txHash, SourceChainId: sourceChainID, + })) + if err == nil { + for _, packet := range res.Msg.GetPacketStatuses() { + last = packet.GetState() + if last == relayerv2.PacketState_PACKET_STATE_SUCCEEDED { + return packet + } + + require.NotEqual( + t, + relayerv2.PacketState_PACKET_STATE_RELAY_FAILED, + last, + "packet permanently failed to relay", + ) + } + } + + select { + case <-ctx.Done(): + t.Fatalf("context cancelled awaiting relay: %v", ctx.Err()) + case <-time.After(500 * time.Millisecond): + } + } + + t.Fatalf("packet did not succeed within budget; last observed state %v", last) + + return nil +} + +// localAttestors converts the environment's attestors into in-process entries. +// Each attestor watches the chain on the far side of the client it backs, and +// signs with the authority key the environment registered in that client's +// on-chain attestation set. +func localAttestors( + t *testing.T, + env *environment.Environment, + runtime environment.Runtime, + keyDir string, +) []ibclink.RelayerAttestor { + t.Helper() + + chains := env.Chains() + require.Len(t, chains, 2, "poc assumes a two chain mesh") + + attestors := make([]ibclink.RelayerAttestor, 0, 2) + + for _, host := range chains { + counterparty := chains[0] + if counterparty == host { + counterparty = chains[1] + } + + // The attestor backing the client on host watches counterparty. + id := meshAttestorFor(host, counterparty) + authority, ok := runtime.Authorities[environment.AuthorityID(id)] + require.True(t, ok, "no authority for attestor %q", id) + + attestors = append(attestors, ibclink.RelayerAttestor{ + Name: string(id), + Type: ibclink.RelayerAttestorLocal, + ChainID: evmChainID(t, env, counterparty), + KeyFile: writeKeyFile(t, keyDir, string(id), authority.PrivateKeyHex), + }) + } + + return attestors +} + +func writeKeyFile(t *testing.T, dir, name, privateKeyHex string) string { + t.Helper() + + require.NoError(t, os.MkdirAll(dir, 0o700)) + + key, err := crypto.HexToECDSA(privateKeyHex) + require.NoError(t, err, "parse attestor key %q", name) + + path := filepath.Join(dir, name+".json") + require.NoError(t, keyfile.Store(path, keyfile.ECDSA, crypto.FromECDSA(key)), "store attestor key") + + return path +} + +// rpcURLForEVMChainID resolves the live RPC endpoint of the chain whose EVM +// chain id matches evmID. +func rpcURLForEVMChainID(t *testing.T, env *environment.Environment, evmID string) string { + t.Helper() + + for _, id := range env.Chains() { + chain, err := env.Chain(id) + require.NoError(t, err, "resolve chain %q", id) + + if strconv.FormatUint(chain.EVMChainID(), 10) == evmID { + return chain.RPCURL() + } + } + + t.Fatalf("no chain with EVM chain id %s", evmID) + + return "" +} + +func evmChainID(t *testing.T, env *environment.Environment, id environment.ChainID) string { + t.Helper() + + chain, err := env.Chain(id) + require.NoError(t, err, "resolve chain %q", id) + + return strconv.FormatUint(chain.EVMChainID(), 10) +} + +func evmChainIDs(t *testing.T, env *environment.Environment) []string { + t.Helper() + + ids := make([]string, 0, len(env.Chains())) + for _, id := range env.Chains() { + ids = append(ids, evmChainID(t, env, id)) + } + + return ids +} + +// pocFactory is a light client type defined entirely outside the relayer, +// registered only through app.Options.Registry. It is never selected by this +// test's config; its presence proves an arbitrary type name is accepted by +// config validation and generator resolution. +type pocFactory struct{} + +type pocParams struct { + ProverURL string `yaml:"proverUrl"` +} + +func (pocFactory) ValidateParams(params *proofgen.RawParams) error { + var p pocParams + + return params.Decode(&p) +} + +func (pocFactory) New( + context.Context, proofgen.Deps, proofgen.ClientEnd, proofgen.ClientEnd, +) (proofgen.ProofGenerator, error) { + return nil, fmt.Errorf("poc-custom-client generates no proofs") +} diff --git a/link/app/app.go b/link/app/app.go new file mode 100644 index 000000000..21e978770 --- /dev/null +++ b/link/app/app.go @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package app is the public entrypoint for running an IBC Link relayer. +// +// It exists so that a custom light client can actually be used: implement +// proofgen.ProofGenerator and proofgen.Factory, register the factory under a +// client type name, and pass the registry here. The relayer's built-in client +// types are registered automatically, so a caller's registry only carries its +// own additions. +// +// reg := proofgen.NewRegistry() +// _ = reg.Register("myclient", myclient.Factory{}) +// +// relayer, err := app.New(ctx, app.Options{ConfigPath: path, Registry: reg}) +// addr, err := relayer.Start(ctx) +// defer relayer.Stop(ctx) +// +// Everything the relayer is built from stays internal; this package deliberately +// exposes only what is needed to start and stop one. +package app + +import ( + "context" + "log/slog" + "net" + + "github.com/pkg/errors" + + "github.com/cosmos/ibc/link/internal/bootstrap" + "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/proofgen" +) + +// Options configure a relayer. +type Options struct { + // ConfigPath is the path to the relayer's YAML config. A path rather than + // a parsed struct, so the config schema stays an internal detail. + ConfigPath string + + // Registry holds light client types beyond the built-ins. May be nil. + // Built-in types are registered regardless and may not be shadowed. + Registry *proofgen.Registry + + // Logger defaults to slog.Default(). + Logger *slog.Logger + + // SkipMigrations starts without applying pending database migrations. + SkipMigrations bool + + // AllowUnknownConfigFields relaxes the strict config decode. Off by + // default: a misspelled key should fail loudly. + AllowUnknownConfigFields bool +} + +// Relayer is a built, not yet running, relayer process. +type Relayer struct { + services *bootstrap.Services + cfg config.Config + logger *slog.Logger + opts Options +} + +// New loads config, resolves every dependency, and validates that each +// configured client end names a registered client type. It does not start +// anything. +func New(_ context.Context, opts Options) (*Relayer, error) { + if opts.ConfigPath == "" { + return nil, errors.New("ConfigPath required") + } + + logger := opts.Logger + if logger == nil { + logger = slog.Default() + } + + cfg, err := config.LoadFromFile(opts.ConfigPath, true, !opts.AllowUnknownConfigFields) + if err != nil { + return nil, errors.Wrapf(err, "loading config %s", opts.ConfigPath) + } + + services, err := bootstrap.BuildRelayer(cfg, opts.Registry) + if err != nil { + return nil, err + } + + return &Relayer{services: services, cfg: cfg, logger: logger, opts: opts}, nil +} + +// Start applies migrations, starts the API server, and starts the relay +// dispatch loop. It returns the address the server is listening on. Start does +// not block; call Stop to shut down. +func (r *Relayer) Start(_ context.Context) (net.Addr, error) { + if err := r.migrate(); err != nil { + return nil, err + } + + address, err := r.services.Server.Start() + if err != nil { + return nil, errors.Wrap(err, "starting relayer server") + } + + if err := r.services.RelayerService.Start(); err != nil { + if stopErr := r.services.Server.Stop(); stopErr != nil { + r.logger.Error("Stopping server after failed dispatch start", "err", stopErr) + } + + return nil, errors.Wrap(err, "starting relayer dispatch loop") + } + + return address, nil +} + +// Stop shuts the relayer down in the reverse of start order: dispatch loop, +// then server, then database. Every step is attempted even if an earlier one +// fails, and the first error is returned. +func (r *Relayer) Stop(_ context.Context) error { + var firstErr error + + record := func(step string, err error) { + if err == nil { + return + } + + r.logger.Error("Stopping relayer", "step", step, "err", err) + + if firstErr == nil { + firstErr = errors.Wrap(err, step) + } + } + + record("relayer service", r.services.RelayerService.Stop()) + record("server", r.services.Server.Stop()) + record("store", r.services.Store.Close()) + + return firstErr +} + +// ChainIDs lists the chains this relayer is configured against, in config +// order. Callers use it to report readiness. +func (r *Relayer) ChainIDs() []string { + chains := make([]string, 0, len(r.cfg.Chains)) + for _, chain := range r.cfg.Chains { + chains = append(chains, chain.ChainID) + } + + return chains +} + +func (r *Relayer) migrate() error { + if r.opts.SkipMigrations { + r.logger.Info("Skipping database migrations") + + return nil + } + + applied, err := r.services.Store.MigrateUp() + if err != nil { + return errors.Wrap(err, "migrating database") + } + + if applied == 0 { + r.logger.Info("No migrations to apply") + } else { + r.logger.Info("Migrated database", "migrations_applied", applied) + } + + return nil +} diff --git a/link/cmd/ibc/relayer.go b/link/cmd/ibc/relayer.go index b7dc7b43d..713848690 100644 --- a/link/cmd/ibc/relayer.go +++ b/link/cmd/ibc/relayer.go @@ -55,7 +55,9 @@ func relayerRun(cmd *cobra.Command, _ []string) error { return err } - app, err := bootstrap.BuildRelayer(cfg) + // nil: the CLI registers no client types beyond the built-ins. Callers + // embedding the relayer pass their own registry via link/app. + app, err := bootstrap.BuildRelayer(cfg, nil) if err != nil { return err } diff --git a/link/internal/bootstrap/bootstrap.go b/link/internal/bootstrap/bootstrap.go index 3abb7c57f..a7712c8d8 100644 --- a/link/internal/bootstrap/bootstrap.go +++ b/link/internal/bootstrap/bootstrap.go @@ -6,7 +6,10 @@ import ( "context" "log/slog" + "github.com/pkg/errors" + "github.com/cosmos/ibc/link/internal/chains" + "github.com/cosmos/ibc/link/internal/clienttypes" "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/dispatch" "github.com/cosmos/ibc/link/internal/relay/pipeline" @@ -18,6 +21,7 @@ import ( "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/txsubmitter" + pgen "github.com/cosmos/ibc/link/proofgen" ) // Services is an outcome of IBC Link wiring (dep inject) @@ -33,8 +37,11 @@ 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) { +// BuildRelayer converts config into a runnable relayer process with all of the deps provisioned. +// +// custom carries light client types registered by the caller beyond the +// built-ins; it may be nil. +func BuildRelayer(cfg config.Config, custom *pgen.Registry) (*Services, error) { ctx := context.Background() logger := slog.With("module", "bootstrap") @@ -78,8 +85,20 @@ func BuildRelayer(cfg config.Config) (*Services, error) { } } + // Light client types: built-ins plus any the caller registered. Client + // types and their params are validated here rather than at load, because + // building the built-in factories needs the attestors resolved above. + clientRegistry, err := clienttypes.Builtins(clientSet, append(local, remote...), custom) + if err != nil { + return nil, err + } + + if err = cfg.ValidateClients(clientRegistry); err != nil { + return nil, errors.Wrap(err, "validation failed") + } + // Proof generators - proofGenerators, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, append(local, remote...)) + proofGenerators, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, clientRegistry) if err != nil { return nil, err } diff --git a/link/internal/clienttypes/clienttypes.go b/link/internal/clienttypes/clienttypes.go new file mode 100644 index 000000000..d33832fb6 --- /dev/null +++ b/link/internal/clienttypes/clienttypes.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package clienttypes assembles the proof generator registry every relayer +// entry point starts from. +// +// It exists so that the built-in light client types are registered identically +// everywhere — the relayer process, live config validation, and the public +// link/app entrypoint — without the generic proofgen package having to import +// any particular client implementation. +package clienttypes + +import ( + "github.com/pkg/errors" + + "github.com/cosmos/ibc/link/internal/chains" + "github.com/cosmos/ibc/link/internal/relay/proofgen/attestation" + "github.com/cosmos/ibc/link/internal/service/attestor" + pgen "github.com/cosmos/ibc/link/proofgen" +) + +// Builtins returns a registry holding the light client types shipped with the +// relayer, wired to the attestors and chain clients resolved from config. +// +// custom may be nil. When supplied, its factories are folded in; a custom type +// may not shadow a built-in name. +func Builtins( + clientSet *chains.ClientSet, + attestors []attestor.Attestor, + custom *pgen.Registry, +) (*pgen.Registry, error) { + reg := pgen.NewRegistry() + + if err := reg.Register(attestation.ClientType, attestation.NewFactory(clientSet, attestors)); err != nil { + return nil, errors.Wrap(err, "registering built-in client types") + } + + if err := reg.Merge(custom); err != nil { + return nil, errors.Wrap(err, "registering custom client types") + } + + return reg, nil +} diff --git a/link/internal/config/clients.go b/link/internal/config/clients.go new file mode 100644 index 000000000..a58a10a52 --- /dev/null +++ b/link/internal/config/clients.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "strings" + + "github.com/pkg/errors" + + "github.com/cosmos/ibc/link/proofgen" +) + +// ValidateClients is the second validation stage: it checks that every +// configured client end names a registered light client type and that its +// params satisfy that type's factory. +// +// It is separate from Validate because it needs a populated registry, and the +// attestation factory can only be built once attestors have been resolved from +// the config Validate already checked. Keeping the structural checks in +// Validate means a malformed database URL or unknown signer alias still fails +// immediately at load, before any chain access. +// +// Every entry point that builds a relayer must call this; link/app does it for +// external callers. +func (c Config) ValidateClients(reg *proofgen.Registry) error { + if reg == nil { + return errors.New("client type registry required") + } + + for _, conn := range c.Relayer.Connections { + for name, end := range map[string]ClientEnd{"clientA": conn.ClientA, "clientB": conn.ClientB} { + if err := validateClientEnd(reg, end); err != nil { + return errors.Wrapf(err, ".relayer.connections[%s].%s", conn.Alias, name) + } + } + } + + return nil +} + +func validateClientEnd(reg *proofgen.Registry, end ClientEnd) error { + factory, ok := reg.Get(string(end.Type)) + if !ok { + return errors.Errorf( + ".type unknown client type %q (registered: [%s])", + end.Type, + strings.Join(reg.Types(), ", "), + ) + } + + if err := factory.ValidateParams(end.Params); err != nil { + return errors.Wrap(err, ".params") + } + + return nil +} diff --git a/link/internal/config/clients_test.go b/link/internal/config/clients_test.go new file mode 100644 index 000000000..86e3031c6 --- /dev/null +++ b/link/internal/config/clients_test.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc/link/proofgen" +) + +type stubParams struct { + ProverURL string `yaml:"proverUrl"` +} + +// stubFactory stands in for a custom light client's factory. +type stubFactory struct { + requireParams bool +} + +func (f stubFactory) ValidateParams(params *proofgen.RawParams) error { + if !f.requireParams { + return nil + } + + var p stubParams + if err := params.Decode(&p); err != nil { + return err + } + + if p.ProverURL == "" { + return errRequired + } + + return nil +} + +func (f stubFactory) New( + context.Context, proofgen.Deps, proofgen.ClientEnd, proofgen.ClientEnd, +) (proofgen.ProofGenerator, error) { + return nil, nil +} + +var errRequired = errorString("proverUrl required") + +type errorString string + +func (e errorString) Error() string { return string(e) } + +func loadSample(t *testing.T) Config { + t.Helper() + + config, err := LoadFromFile(filepath.Join("testdata", "sample.yml"), true, true) + require.NoError(t, err) + + return config +} + +func TestValidateClients(t *testing.T) { + t.Run("registered type passes", func(t *testing.T) { + config := loadSample(t) + + reg := proofgen.NewRegistry() + require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{})) + + require.NoError(t, config.ValidateClients(reg)) + }) + + t.Run("unregistered type fails and lists what is registered", func(t *testing.T) { + config := loadSample(t) + config.Relayer.Connections[0].ClientA.Type = "tendermint" + + reg := proofgen.NewRegistry() + require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{})) + + err := config.ValidateClients(reg) + require.ErrorContains(t, err, `unknown client type "tendermint"`) + require.ErrorContains(t, err, "attestation") + }) + + t.Run("arbitrary type name resolves once registered", func(t *testing.T) { + config := loadSample(t) + for i := range config.Relayer.Connections { + config.Relayer.Connections[i].ClientA.Type = "myclient" + config.Relayer.Connections[i].ClientB.Type = "myclient" + } + + reg := proofgen.NewRegistry() + require.NoError(t, reg.Register("myclient", stubFactory{})) + + require.NoError(t, config.ValidateClients(reg)) + }) + + t.Run("factory params error is surfaced", func(t *testing.T) { + config := loadSample(t) + + reg := proofgen.NewRegistry() + require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{requireParams: true})) + + err := config.ValidateClients(reg) + require.ErrorContains(t, err, ".params") + require.ErrorContains(t, err, "proverUrl required") + }) + + t.Run("nil registry is an error, not a silent pass", func(t *testing.T) { + config := loadSample(t) + + require.Error(t, config.ValidateClients(nil)) + }) +} + +// 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 := proofgen.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 := proofgen.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 *proofgen.RawParams + + var p stubParams + require.NoError(t, params.Decode(&p)) + require.True(t, params.IsEmpty()) +} diff --git a/link/internal/config/relayer.go b/link/internal/config/relayer.go index e0ea9e183..7d2c28dc8 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/proofgen" ) // ClientType the light client type. @@ -64,6 +66,11 @@ type ClientEnd struct { ClientID string `yaml:"clientId"` Type ClientType `yaml:"type"` + // Params is the client-type-specific config block, captured verbatim and + // interpreted by the proof generator factory registered for Type. A + // pointer so that adding it keeps ClientEnd comparable. + Params *proofgen.RawParams `yaml:"params,omitempty"` + // AutoRelay configures auto-relay for packets flowing FROM this end's // chain TOWARD the counterparty end. AutoRelay AutoRelayConfig `yaml:"autoRelay,omitempty"` @@ -185,8 +192,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..80d8a2fff 100644 --- a/link/internal/config/relayer_test.go +++ b/link/internal/config/relayer_test.go @@ -198,11 +198,20 @@ func TestRelayerConfig(t *testing.T) { errContains: ".chainId required", }, { - name: "unsupported client type", + // An unregistered type is no longer a structural error: client + // types are open, and validating one needs a registry. See + // TestValidateClients for the check that replaced this. + 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..262ceed6c 100644 --- a/link/internal/livevalidate/quorum.go +++ b/link/internal/livevalidate/quorum.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" + "github.com/cosmos/ibc/link/internal/clienttypes" "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/service/attestor" @@ -33,7 +34,16 @@ 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 { + reg, err := clienttypes.Builtins(clientSet, attestors, nil) + if err != nil { + return errors.Wrap(err, "client types") + } + + if err := cfg.ValidateClients(reg); err != nil { + return errors.Wrap(err, "client config") + } + + if _, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, reg); 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..053642a2a 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, `unknown client type "tendermint"`) + require.ErrorContains(t, err, "attestation", "the error should name what is registered") }) } diff --git a/link/internal/relay/proofgen/attestation/factory.go b/link/internal/relay/proofgen/attestation/factory.go new file mode 100644 index 000000000..eab5f3661 --- /dev/null +++ b/link/internal/relay/proofgen/attestation/factory.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 + +package attestation + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/cosmos/ibc/link/internal/chains" + "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/internal/service/attestor" + pgen "github.com/cosmos/ibc/link/proofgen" +) + +// ClientType is the config `type:` name this factory registers under. +const ClientType = string(config.ClientTypeAttestation) + +var ( + _ pgen.Factory = factory{} + _ pgen.ProofGenerator = (*Generator)(nil) +) + +// factory builds attestation proof generators. It closes over the attestor set +// and chain clients it needs, so the generic registry never carries +// attestation-specific arguments. +type factory struct { + clientSet *chains.ClientSet + attestors []attestor.Attestor +} + +// NewFactory returns the built-in attestation client factory. attestors is +// every attestor this process can reach, local and remote; which of them back +// a given client is resolved per client end against on-chain state. +func NewFactory(clientSet *chains.ClientSet, attestors []attestor.Attestor) pgen.Factory { + return factory{clientSet: clientSet, attestors: attestors} +} + +// ValidateParams rejects params outright: the attestation client is configured +// through the top-level `attestors` block and its on-chain attestation set, not +// through per-client params. +func (f factory) ValidateParams(params *pgen.RawParams) error { + if !params.IsEmpty() { + return errors.New("attestation clients take no params; configure attestors under .attestors") + } + + return nil +} + +// New resolves the attestor quorum for self and builds its generator. Deps are +// unused: this factory already holds richer chain access than the public +// CounterpartyChain view exposes, because it must query self's on-chain +// attestation set. +func (f factory) New( + ctx context.Context, + _ pgen.Deps, + self, counterparty pgen.ClientEnd, +) (pgen.ProofGenerator, error) { + return ResolveGenerator(ctx, toConfigEnd(self), toConfigEnd(counterparty), f.clientSet, f.attestors) +} + +func toConfigEnd(end pgen.ClientEnd) config.ClientEnd { + return config.ClientEnd{ + ChainID: end.ChainID, + ClientID: end.ClientID, + Type: config.ClientType(end.Type), + } +} diff --git a/link/internal/relay/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index 9f7d59489..8712b903e 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -7,41 +7,20 @@ 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" + pgen "github.com/cosmos/ibc/link/proofgen" ) // 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) +// +// Defined in the public proofgen package so custom light clients can implement +// it; aliased here so internal callers are unaffected. +type ProofGenerator = pgen.ProofGenerator // Key identifies one configured light client by the chain it lives on and // its client id, the composite key ProofGenerator instances are scoped by. @@ -68,18 +47,18 @@ func (s *Set) Get(chainID, clientID string) (ProofGenerator, bool) { } // 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). +// configured connection, dispatching to the factory registered for each end's +// client type. func NewSetFromConfig( ctx context.Context, cfg config.Config, clientSet *chains.ClientSet, - attestors []attestor.Attestor, + reg *pgen.Registry, ) (*Set, error) { generators := make(map[string]ProofGenerator, len(cfg.Relayer.Connections)*2) 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, clientSet, reg) }) if err != nil { return nil, err @@ -113,19 +92,44 @@ func addGenerator( connAlias string, client, clientCounterparty config.ClientEnd, clientSet *chains.ClientSet, - attestors []attestor.Attestor, + reg *pgen.Registry, ) error { - switch client.Type { - case config.ClientTypeAttestation: - gen, err := attestation.ResolveGenerator(ctx, client, clientCounterparty, clientSet, attestors) - if err != nil { - return err - } + factory, ok := reg.Get(string(client.Type)) + if !ok { + return errors.Errorf( + "connection %q: no proof generator registered for client type %q (registered: %v)", + connAlias, client.Type, reg.Types(), + ) + } + + counterpartyChain, ok := clientSet.Get(clientCounterparty.ChainID) + if !ok { + return errors.Errorf( + "connection %q: no configured chain client for counterparty chain %q", + connAlias, clientCounterparty.ChainID, + ) + } - generators[Key(client.ChainID, client.ClientID)] = gen + gen, err := factory.New( + ctx, + pgen.Deps{Counterparty: counterpartyChain}, + toPublicEnd(client), + toPublicEnd(clientCounterparty), + ) + if err != nil { + return errors.Wrapf(err, "connection %q", connAlias) + } + + generators[Key(client.ChainID, client.ClientID)] = gen + + return nil +} - return nil - default: - return errors.Errorf("connection %q: unsupported client type %q for proof generation", connAlias, client.Type) +func toPublicEnd(end config.ClientEnd) pgen.ClientEnd { + return pgen.ClientEnd{ + ChainID: end.ChainID, + ClientID: end.ClientID, + Type: string(end.Type), + Params: end.Params, } } diff --git a/link/internal/relay/proofgen/proofgen_test.go b/link/internal/relay/proofgen/proofgen_test.go index 963af37fe..2177c1de6 100644 --- a/link/internal/relay/proofgen/proofgen_test.go +++ b/link/internal/relay/proofgen/proofgen_test.go @@ -10,8 +10,10 @@ import ( "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" "github.com/cosmos/ibc/link/internal/tests/mocks" + pgen "github.com/cosmos/ibc/link/proofgen" ) func testConnection() config.ConnectionConfig { @@ -85,7 +87,7 @@ func TestNewSetFromConfig(t *testing.T) { cfg, clientSet, attestors := testConfig(t) conn := cfg.Relayer.Connections[0] - set, err := NewSetFromConfig(ctx, cfg, clientSet, attestors) + set, err := NewSetFromConfig(ctx, cfg, clientSet, testRegistry(t, clientSet, attestors)) require.NoError(t, err) _, ok := set.Get(conn.ClientA.ChainID, conn.ClientA.ClientID) @@ -109,9 +111,58 @@ func TestNewSetFromConfig(t *testing.T) { cfg := config.Config{Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}} // ACT - _, err := NewSetFromConfig(ctx, cfg, clientSet, nil) + _, err := NewSetFromConfig(ctx, cfg, clientSet, testRegistry(t, clientSet, nil)) // ASSERT - require.ErrorContains(t, err, `unsupported client type "tendermint"`) + require.ErrorContains(t, err, `no proof generator 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{Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}} + + reg := pgen.NewRegistry() + require.NoError(t, reg.Register("myclient", stubFactory{})) + + set, err := NewSetFromConfig(ctx, cfg, clientSet, reg) + require.NoError(t, err) + + _, ok := set.Get(conn.ClientA.ChainID, conn.ClientA.ClientID) + require.True(t, ok) }) } + +// testRegistry builds the built-in registry the relayer normally assembles at +// startup. +func testRegistry(t *testing.T, clientSet *chains.ClientSet, attestors []attestor.Attestor) *pgen.Registry { + t.Helper() + + reg := pgen.NewRegistry() + require.NoError(t, reg.Register(attestation.ClientType, attestation.NewFactory(clientSet, attestors))) + + return reg +} + +// stubFactory is a light client type that exists only in this test. +type stubFactory struct{} + +func (stubFactory) ValidateParams(*pgen.RawParams) error { return nil } + +func (stubFactory) New( + context.Context, pgen.Deps, pgen.ClientEnd, pgen.ClientEnd, +) (pgen.ProofGenerator, error) { + return stubGenerator{}, nil +} + +type stubGenerator struct{ pgen.ProofGenerator } diff --git a/link/internal/types/v2/client.go b/link/internal/types/v2/client.go index 122303193..824ecc612 100644 --- a/link/internal/types/v2/client.go +++ b/link/internal/types/v2/client.go @@ -4,14 +4,16 @@ package v2 import ( "math" - "time" + + "github.com/cosmos/ibc/link/proofgen" ) -// BlockHeader represents a minimal subset of fields that IBC client needs for *attestation*. -type BlockHeader struct { - Height uint64 - Timestamp time.Time -} +// BlockHeader is the minimal view of a block the attestor and proof generators +// need: a height and its timestamp. +// +// Defined in the public proofgen package so custom light clients can implement +// against it; aliased here so internal callers are unaffected. +type BlockHeader = proofgen.BlockHeader // Special markers for different block heights. const ( diff --git a/link/internal/types/v2/types.go b/link/internal/types/v2/types.go index faba22dd7..a82f16197 100644 --- a/link/internal/types/v2/types.go +++ b/link/internal/types/v2/types.go @@ -7,6 +7,8 @@ import ( "time" channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" + + "github.com/cosmos/ibc/link/proofgen" ) // EventKind the kind of packet event. @@ -30,14 +32,17 @@ const ( ) // ProofKind the kind of packet claim a proof attests to. -type ProofKind int +// +// Defined in the public proofgen package so custom light clients can implement +// against it; aliased here so internal callers are unaffected. +type ProofKind = proofgen.ProofKind // Proof kinds const ( - ProofKindUnknown ProofKind = iota - ProofKindPacketCommitment - ProofKindAcknowledgement - ProofKindReceiptAbsence + ProofKindUnknown = proofgen.ProofKindUnknown + ProofKindPacketCommitment = proofgen.ProofKindPacketCommitment + ProofKindAcknowledgement = proofgen.ProofKindAcknowledgement + ProofKindReceiptAbsence = proofgen.ProofKindReceiptAbsence ) // RelayKind the packet operation one PacketRelayItem asks to perform. diff --git a/link/proofgen/params.go b/link/proofgen/params.go new file mode 100644 index 000000000..a83a38333 --- /dev/null +++ b/link/proofgen/params.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +package proofgen + +import ( + "github.com/goccy/go-yaml" + "github.com/pkg/errors" +) + +// RawParams is a client-type-specific config block captured verbatim at decode +// time and interpreted by the Factory registered for that type, not by the +// config package. +// +// It is used as a pointer in config structs so that adding it does not make +// those structs non-comparable. +type RawParams struct { + raw []byte +} + +// NewRawParams builds params from an already-encoded YAML document. Mainly +// useful in tests. +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("proofgen: 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 no params were configured. A Factory that requires +// params should check this and return a helpful error rather than decoding an +// empty document into a zero value. +func (p *RawParams) IsEmpty() bool { + return p == nil || len(p.raw) == 0 +} + +// Decode unmarshals the params into v, rejecting fields v does not declare. +// +// Rejecting unknown fields matters: the top-level config decode cannot see +// inside a captured block, so without this a misspelled key would silently +// become a zero value. Factories should always decode through this method +// rather than unmarshalling p.Bytes() themselves. +// +// Decoding empty params is a no-op, leaving v at its defaults. +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. Prefer Decode, which applies strict +// field checking. +func (p *RawParams) Bytes() []byte { + if p == nil { + return nil + } + + return p.raw +} diff --git a/link/proofgen/proofgen.go b/link/proofgen/proofgen.go new file mode 100644 index 000000000..330460d00 --- /dev/null +++ b/link/proofgen/proofgen.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package proofgen is the public extension point for light client proof +// generation. Implement ProofGenerator and Factory here, register the factory +// under a client type name, and start a relayer with it via link/app. +// +// This package must not import anything under link/internal. Everything it +// exposes is deliberately narrow: proofs are opaque bytes, and the relayer +// never interprets them. +package proofgen + +import ( + "context" + "time" + + channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" +) + +// 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. The + // returned bytes are passed to the light client's updateClient entrypoint + // unmodified. + 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 ProofKind, + packets []channeltypesv2.Packet, + ) ([][]byte, error) +} + +// ProofKind the kind of packet claim a proof attests to. +type ProofKind int + +// Proof kinds. +const ( + ProofKindUnknown ProofKind = iota + ProofKindPacketCommitment + ProofKindAcknowledgement + ProofKindReceiptAbsence +) + +// BlockHeader is the minimal view of a counterparty block a proof generator +// needs to reason about heights and timestamps. +type BlockHeader struct { + Height uint64 + Timestamp time.Time +} + +// CounterpartyChain reads state from the chain a light client tracks. It is a +// deliberately narrow view of the relayer's full chain client; it may gain +// methods over time, which is safe because implementations live in the relayer +// and generators only consume it. +type CounterpartyChain interface { + ChainID() string + + // GetBlockHeader returns the header at height. The special height markers + // used internally by the relayer are not part of this contract; pass a + // concrete height. + GetBlockHeader(ctx context.Context, height uint64) (BlockHeader, error) +} + +// Deps are the relayer-provided dependencies handed to a Factory. +type Deps struct { + // Counterparty reads the chain this client tracks. + Counterparty CounterpartyChain +} + +// ClientEnd is one side of a configured connection: a light client on ChainID, +// tracking the connection's other end as its counterparty. +type ClientEnd struct { + ChainID string + ClientID string + Type string + Params *RawParams +} diff --git a/link/proofgen/registry.go b/link/proofgen/registry.go new file mode 100644 index 000000000..dc260cefc --- /dev/null +++ b/link/proofgen/registry.go @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 + +package proofgen + +import ( + "context" + "sort" + + "github.com/pkg/errors" +) + +// Factory builds proof generators for one light client type and validates that +// type's config params. +type Factory interface { + // ValidateParams reports whether params are usable for this client type. + // It runs during config validation, before any chain access, so it must + // not perform I/O. Decode through RawParams.Decode so unknown fields are + // rejected. + ValidateParams(params *RawParams) error + + // New builds a generator for self, which tracks counterparty. Params have + // already passed ValidateParams. + New(ctx context.Context, deps Deps, self, counterparty ClientEnd) (ProofGenerator, error) +} + +// Registry resolves a Factory by client type name. The relayer registers its +// built-in types; callers add their own before starting a relayer. +// +// A Registry is not safe for concurrent registration. Build it fully during +// startup, then treat it as read-only. +type Registry struct { + factories map[string]Factory +} + +func NewRegistry() *Registry { + return &Registry{factories: make(map[string]Factory)} +} + +// Register associates a client type name with a factory. The name is the +// string operators write as `type:` in their connection config. Registering a +// name twice is an error rather than a silent overwrite. +func (r *Registry) Register(clientType string, f Factory) error { + switch { + case clientType == "": + return errors.New("client type must not be empty") + case f == nil: + return errors.Errorf("client type %q: factory must not be nil", 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) (Factory, bool) { + if r == nil { + return nil, false + } + + f, ok := r.factories[clientType] + + return f, ok +} + +// Merge registers every entry of other into r, failing on any name r already +// has. Used to fold caller-supplied custom types into the relayer's built-ins. +func (r *Registry) Merge(other *Registry) error { + if other == nil { + return nil + } + + for name, f := range other.factories { + if err := r.Register(name, f); err != nil { + return err + } + } + + return nil +} + +// Types lists every registered client type, sorted. Useful for error messages. +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 +} From 63ad7e76594a32bc6647e8bde1928e09aea83fe2 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 14:47:10 -0700 Subject: [PATCH 02/17] feat(link): support custom light clients in compiled CLI Signed-off-by: Dennis Fang --- link/app/app.go | 168 ------------- link/{cmd/ibc => cli}/AGENTS.md | 4 +- link/{cmd/ibc => cli}/attestor.go | 2 +- link/{cmd/ibc => cli}/attestor_test.go | 2 +- link/{cmd/ibc => cli}/attestors.go | 2 +- link/{cmd/ibc => cli}/attestors_test.go | 2 +- link/{cmd/ibc => cli}/config.go | 2 +- link/{cmd/ibc => cli}/config_test.go | 2 +- link/{cmd/ibc => cli}/deploy.go | 2 +- link/{cmd/ibc => cli}/deploy_test.go | 2 +- link/{cmd/ibc => cli}/ift.go | 2 +- link/{cmd/ibc => cli}/ift_test.go | 2 +- link/{cmd/ibc => cli}/keys.go | 2 +- link/{cmd/ibc => cli}/migrate.go | 2 +- link/{cmd/ibc => cli}/query.go | 2 +- link/{cmd/ibc => cli}/relayer.go | 9 +- link/cli/root.go | 223 ++++++++++++++++++ link/{cmd/ibc => cli}/tx.go | 2 +- link/cmd/ibc/main.go | 195 +-------------- link/internal/bootstrap/bootstrap.go | 29 +-- link/internal/clienttypes/clienttypes.go | 42 ---- link/internal/config/clients.go | 39 +-- link/internal/config/clients_test.go | 56 +++-- link/internal/config/relayer.go | 4 +- link/internal/livevalidate/quorum.go | 10 +- .../processors/batch_recv_packet_test.go | 7 +- link/internal/relay/processors/batch_relay.go | 11 +- .../relay/proofgen/attestation/factory.go | 68 ------ .../relay/proofgen/attestation/generator.go | 14 +- .../proofgen/attestation/generator_test.go | 5 +- link/internal/relay/proofgen/proofgen.go | 66 +++--- link/internal/relay/proofgen/proofgen_test.go | 30 +-- link/internal/relay/txbuilder/evm/evm.go | 2 +- link/internal/tests/mocks/proofgen.go | 20 +- link/internal/types/v2/client.go | 11 +- link/internal/types/v2/types.go | 16 -- link/{proofgen => lightclient}/params.go | 4 +- link/{proofgen => lightclient}/proofgen.go | 50 ++-- link/{proofgen => lightclient}/registry.go | 28 +-- 39 files changed, 423 insertions(+), 716 deletions(-) delete mode 100644 link/app/app.go rename link/{cmd/ibc => cli}/AGENTS.md (90%) rename link/{cmd/ibc => cli}/attestor.go (99%) rename link/{cmd/ibc => cli}/attestor_test.go (99%) rename link/{cmd/ibc => cli}/attestors.go (99%) rename link/{cmd/ibc => cli}/attestors_test.go (99%) rename link/{cmd/ibc => cli}/config.go (99%) rename link/{cmd/ibc => cli}/config_test.go (97%) rename link/{cmd/ibc => cli}/deploy.go (99%) rename link/{cmd/ibc => cli}/deploy_test.go (99%) rename link/{cmd/ibc => cli}/ift.go (99%) rename link/{cmd/ibc => cli}/ift_test.go (97%) rename link/{cmd/ibc => cli}/keys.go (99%) rename link/{cmd/ibc => cli}/migrate.go (99%) rename link/{cmd/ibc => cli}/query.go (93%) rename link/{cmd/ibc => cli}/relayer.go (95%) create mode 100644 link/cli/root.go rename link/{cmd/ibc => cli}/tx.go (92%) delete mode 100644 link/internal/clienttypes/clienttypes.go delete mode 100644 link/internal/relay/proofgen/attestation/factory.go rename link/{proofgen => lightclient}/params.go (95%) rename link/{proofgen => lightclient}/proofgen.go (55%) rename link/{proofgen => lightclient}/registry.go (71%) diff --git a/link/app/app.go b/link/app/app.go deleted file mode 100644 index 21e978770..000000000 --- a/link/app/app.go +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Package app is the public entrypoint for running an IBC Link relayer. -// -// It exists so that a custom light client can actually be used: implement -// proofgen.ProofGenerator and proofgen.Factory, register the factory under a -// client type name, and pass the registry here. The relayer's built-in client -// types are registered automatically, so a caller's registry only carries its -// own additions. -// -// reg := proofgen.NewRegistry() -// _ = reg.Register("myclient", myclient.Factory{}) -// -// relayer, err := app.New(ctx, app.Options{ConfigPath: path, Registry: reg}) -// addr, err := relayer.Start(ctx) -// defer relayer.Stop(ctx) -// -// Everything the relayer is built from stays internal; this package deliberately -// exposes only what is needed to start and stop one. -package app - -import ( - "context" - "log/slog" - "net" - - "github.com/pkg/errors" - - "github.com/cosmos/ibc/link/internal/bootstrap" - "github.com/cosmos/ibc/link/internal/config" - "github.com/cosmos/ibc/link/proofgen" -) - -// Options configure a relayer. -type Options struct { - // ConfigPath is the path to the relayer's YAML config. A path rather than - // a parsed struct, so the config schema stays an internal detail. - ConfigPath string - - // Registry holds light client types beyond the built-ins. May be nil. - // Built-in types are registered regardless and may not be shadowed. - Registry *proofgen.Registry - - // Logger defaults to slog.Default(). - Logger *slog.Logger - - // SkipMigrations starts without applying pending database migrations. - SkipMigrations bool - - // AllowUnknownConfigFields relaxes the strict config decode. Off by - // default: a misspelled key should fail loudly. - AllowUnknownConfigFields bool -} - -// Relayer is a built, not yet running, relayer process. -type Relayer struct { - services *bootstrap.Services - cfg config.Config - logger *slog.Logger - opts Options -} - -// New loads config, resolves every dependency, and validates that each -// configured client end names a registered client type. It does not start -// anything. -func New(_ context.Context, opts Options) (*Relayer, error) { - if opts.ConfigPath == "" { - return nil, errors.New("ConfigPath required") - } - - logger := opts.Logger - if logger == nil { - logger = slog.Default() - } - - cfg, err := config.LoadFromFile(opts.ConfigPath, true, !opts.AllowUnknownConfigFields) - if err != nil { - return nil, errors.Wrapf(err, "loading config %s", opts.ConfigPath) - } - - services, err := bootstrap.BuildRelayer(cfg, opts.Registry) - if err != nil { - return nil, err - } - - return &Relayer{services: services, cfg: cfg, logger: logger, opts: opts}, nil -} - -// Start applies migrations, starts the API server, and starts the relay -// dispatch loop. It returns the address the server is listening on. Start does -// not block; call Stop to shut down. -func (r *Relayer) Start(_ context.Context) (net.Addr, error) { - if err := r.migrate(); err != nil { - return nil, err - } - - address, err := r.services.Server.Start() - if err != nil { - return nil, errors.Wrap(err, "starting relayer server") - } - - if err := r.services.RelayerService.Start(); err != nil { - if stopErr := r.services.Server.Stop(); stopErr != nil { - r.logger.Error("Stopping server after failed dispatch start", "err", stopErr) - } - - return nil, errors.Wrap(err, "starting relayer dispatch loop") - } - - return address, nil -} - -// Stop shuts the relayer down in the reverse of start order: dispatch loop, -// then server, then database. Every step is attempted even if an earlier one -// fails, and the first error is returned. -func (r *Relayer) Stop(_ context.Context) error { - var firstErr error - - record := func(step string, err error) { - if err == nil { - return - } - - r.logger.Error("Stopping relayer", "step", step, "err", err) - - if firstErr == nil { - firstErr = errors.Wrap(err, step) - } - } - - record("relayer service", r.services.RelayerService.Stop()) - record("server", r.services.Server.Stop()) - record("store", r.services.Store.Close()) - - return firstErr -} - -// ChainIDs lists the chains this relayer is configured against, in config -// order. Callers use it to report readiness. -func (r *Relayer) ChainIDs() []string { - chains := make([]string, 0, len(r.cfg.Chains)) - for _, chain := range r.cfg.Chains { - chains = append(chains, chain.ChainID) - } - - return chains -} - -func (r *Relayer) migrate() error { - if r.opts.SkipMigrations { - r.logger.Info("Skipping database migrations") - - return nil - } - - applied, err := r.services.Store.MigrateUp() - if err != nil { - return errors.Wrap(err, "migrating database") - } - - if applied == 0 { - r.logger.Info("No migrations to apply") - } else { - r.logger.Info("Migrated database", "migrations_applied", applied) - } - - return nil -} 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 713848690..6192323ee 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,9 +56,9 @@ func relayerRun(cmd *cobra.Command, _ []string) error { return err } - // nil: the CLI registers no client types beyond the built-ins. Callers - // embedding the relayer pass their own registry via link/app. - app, err := bootstrap.BuildRelayer(cfg, nil) + app, err := bootstrap.BuildRelayer(cfg, bootstrap.RelayerOptions{ + LightClients: relayerOptions.LightClients, + }) if err != nil { return err } diff --git a/link/cli/root.go b/link/cli/root.go new file mode 100644 index 000000000..73145cd82 --- /dev/null +++ b/link/cli/root.go @@ -0,0 +1,223 @@ +// 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 supplies dependencies for command families in the IBC CLI. +type Options struct { + Relayer RelayerOptions +} + +// RelayerOptions supplies extensions used by the relayer commands. +type RelayerOptions struct { + // LightClients contains client types beyond the relayer's built-ins. + LightClients *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 complete IBC CLI with the supplied extensions. +// It is intended to be called once by an executable's main function. +func NewRootCmd(opts Options) *cobra.Command { + relayerOptions = opts.Relayer + + return rootCmd +} + +// Execute runs root with signal cancellation and returns a process 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 a7712c8d8..b4f914a00 100644 --- a/link/internal/bootstrap/bootstrap.go +++ b/link/internal/bootstrap/bootstrap.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/clienttypes" "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/dispatch" "github.com/cosmos/ibc/link/internal/relay/pipeline" @@ -21,7 +20,7 @@ import ( "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/txsubmitter" - pgen "github.com/cosmos/ibc/link/proofgen" + "github.com/cosmos/ibc/link/lightclient" ) // Services is an outcome of IBC Link wiring (dep inject) @@ -37,11 +36,15 @@ type Services struct { AttestorService *attestor.Service } +// RelayerOptions supplies optional relayer extensions. +type RelayerOptions struct { + // LightClients contains custom client types. Built-in attestation clients + // are resolved internally. + LightClients *lightclient.Registry +} + // BuildRelayer converts config into a runnable relayer process with all of the deps provisioned. -// -// custom carries light client types registered by the caller beyond the -// built-ins; it may be nil. -func BuildRelayer(cfg config.Config, custom *pgen.Registry) (*Services, error) { +func BuildRelayer(cfg config.Config, opts RelayerOptions) (*Services, error) { ctx := context.Background() logger := slog.With("module", "bootstrap") @@ -85,20 +88,14 @@ func BuildRelayer(cfg config.Config, custom *pgen.Registry) (*Services, error) { } } - // Light client types: built-ins plus any the caller registered. Client - // types and their params are validated here rather than at load, because - // building the built-in factories needs the attestors resolved above. - clientRegistry, err := clienttypes.Builtins(clientSet, append(local, remote...), custom) - if err != nil { - return nil, err - } - - if err = cfg.ValidateClients(clientRegistry); err != nil { + if err = cfg.ValidateClients(opts.LightClients); err != nil { return nil, errors.Wrap(err, "validation failed") } // Proof generators - proofGenerators, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, clientRegistry) + proofGenerators, err := proofgen.NewSetFromConfig( + ctx, cfg, clientSet, append(local, remote...), opts.LightClients, + ) if err != nil { return nil, err } diff --git a/link/internal/clienttypes/clienttypes.go b/link/internal/clienttypes/clienttypes.go deleted file mode 100644 index d33832fb6..000000000 --- a/link/internal/clienttypes/clienttypes.go +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Package clienttypes assembles the proof generator registry every relayer -// entry point starts from. -// -// It exists so that the built-in light client types are registered identically -// everywhere — the relayer process, live config validation, and the public -// link/app entrypoint — without the generic proofgen package having to import -// any particular client implementation. -package clienttypes - -import ( - "github.com/pkg/errors" - - "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/relay/proofgen/attestation" - "github.com/cosmos/ibc/link/internal/service/attestor" - pgen "github.com/cosmos/ibc/link/proofgen" -) - -// Builtins returns a registry holding the light client types shipped with the -// relayer, wired to the attestors and chain clients resolved from config. -// -// custom may be nil. When supplied, its factories are folded in; a custom type -// may not shadow a built-in name. -func Builtins( - clientSet *chains.ClientSet, - attestors []attestor.Attestor, - custom *pgen.Registry, -) (*pgen.Registry, error) { - reg := pgen.NewRegistry() - - if err := reg.Register(attestation.ClientType, attestation.NewFactory(clientSet, attestors)); err != nil { - return nil, errors.Wrap(err, "registering built-in client types") - } - - if err := reg.Merge(custom); err != nil { - return nil, errors.Wrap(err, "registering custom client types") - } - - return reg, nil -} diff --git a/link/internal/config/clients.go b/link/internal/config/clients.go index a58a10a52..6107a5aec 100644 --- a/link/internal/config/clients.go +++ b/link/internal/config/clients.go @@ -7,24 +7,16 @@ import ( "github.com/pkg/errors" - "github.com/cosmos/ibc/link/proofgen" + "github.com/cosmos/ibc/link/lightclient" ) -// ValidateClients is the second validation stage: it checks that every -// configured client end names a registered light client type and that its -// params satisfy that type's factory. +// ValidateClients checks built-in attestation parameters and validates custom +// client ends through the caller-supplied registry. // -// It is separate from Validate because it needs a populated registry, and the -// attestation factory can only be built once attestors have been resolved from -// the config Validate already checked. Keeping the structural checks in -// Validate means a malformed database URL or unknown signer alias still fails -// immediately at load, before any chain access. -// -// Every entry point that builds a relayer must call this; link/app does it for -// external callers. -func (c Config) ValidateClients(reg *proofgen.Registry) error { - if reg == nil { - return errors.New("client type registry required") +// Every entry point that builds a relayer must call this. +func (c Config) ValidateClients(reg *lightclient.Registry) error { + if _, shadowsBuiltin := reg.Get(string(ClientTypeAttestation)); shadowsBuiltin { + return errors.Errorf("client type %q is built in and cannot be overridden", ClientTypeAttestation) } for _, conn := range c.Relayer.Connections { @@ -38,18 +30,27 @@ func (c Config) ValidateClients(reg *proofgen.Registry) error { return nil } -func validateClientEnd(reg *proofgen.Registry, end ClientEnd) error { +func validateClientEnd(reg *lightclient.Registry, end ClientEnd) error { + if end.Type == ClientTypeAttestation { + if !end.ClientParams.IsEmpty() { + return errors.New(".clientParams attestation clients take no params; configure attestors under .attestors") + } + + return nil + } + factory, ok := reg.Get(string(end.Type)) if !ok { + registered := append([]string{string(ClientTypeAttestation)}, reg.Types()...) return errors.Errorf( ".type unknown client type %q (registered: [%s])", end.Type, - strings.Join(reg.Types(), ", "), + strings.Join(registered, ", "), ) } - if err := factory.ValidateParams(end.Params); err != nil { - return errors.Wrap(err, ".params") + if err := factory.ValidateParams(end.ClientParams); err != nil { + return errors.Wrap(err, ".clientParams") } return nil diff --git a/link/internal/config/clients_test.go b/link/internal/config/clients_test.go index 86e3031c6..a8581d3e8 100644 --- a/link/internal/config/clients_test.go +++ b/link/internal/config/clients_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/link/proofgen" + "github.com/cosmos/ibc/link/lightclient" ) type stubParams struct { @@ -21,7 +21,7 @@ type stubFactory struct { requireParams bool } -func (f stubFactory) ValidateParams(params *proofgen.RawParams) error { +func (f stubFactory) ValidateParams(params *lightclient.RawParams) error { if !f.requireParams { return nil } @@ -39,8 +39,8 @@ func (f stubFactory) ValidateParams(params *proofgen.RawParams) error { } func (f stubFactory) New( - context.Context, proofgen.Deps, proofgen.ClientEnd, proofgen.ClientEnd, -) (proofgen.ProofGenerator, error) { + context.Context, lightclient.FactoryOptions, +) (lightclient.ProofGenerator, error) { return nil, nil } @@ -63,22 +63,15 @@ func TestValidateClients(t *testing.T) { t.Run("registered type passes", func(t *testing.T) { config := loadSample(t) - reg := proofgen.NewRegistry() - require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{})) - - require.NoError(t, config.ValidateClients(reg)) + require.NoError(t, config.ValidateClients(nil)) }) t.Run("unregistered type fails and lists what is registered", func(t *testing.T) { config := loadSample(t) config.Relayer.Connections[0].ClientA.Type = "tendermint" - reg := proofgen.NewRegistry() - require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{})) - - err := config.ValidateClients(reg) + err := config.ValidateClients(nil) require.ErrorContains(t, err, `unknown client type "tendermint"`) - require.ErrorContains(t, err, "attestation") }) t.Run("arbitrary type name resolves once registered", func(t *testing.T) { @@ -88,7 +81,7 @@ func TestValidateClients(t *testing.T) { config.Relayer.Connections[i].ClientB.Type = "myclient" } - reg := proofgen.NewRegistry() + reg := lightclient.NewRegistry() require.NoError(t, reg.Register("myclient", stubFactory{})) require.NoError(t, config.ValidateClients(reg)) @@ -96,19 +89,38 @@ func TestValidateClients(t *testing.T) { t.Run("factory params error is surfaced", func(t *testing.T) { config := loadSample(t) + for i := range config.Relayer.Connections { + config.Relayer.Connections[i].ClientA.Type = "myclient" + config.Relayer.Connections[i].ClientB.Type = "myclient" + } - reg := proofgen.NewRegistry() - require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{requireParams: true})) + reg := lightclient.NewRegistry() + require.NoError(t, reg.Register("myclient", stubFactory{requireParams: true})) err := config.ValidateClients(reg) - require.ErrorContains(t, err, ".params") + require.ErrorContains(t, err, ".clientParams") require.ErrorContains(t, err, "proverUrl required") }) - t.Run("nil registry is an error, not a silent pass", func(t *testing.T) { + t.Run("nil registry supports built-in attestation", func(t *testing.T) { + config := loadSample(t) + + require.NoError(t, config.ValidateClients(nil)) + }) + + t.Run("attestation name cannot be overridden", func(t *testing.T) { + config := loadSample(t) + reg := lightclient.NewRegistry() + require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{})) + + require.ErrorContains(t, config.ValidateClients(reg), "cannot be overridden") + }) + + t.Run("attestation params are rejected without a factory", func(t *testing.T) { config := loadSample(t) + config.Relayer.Connections[0].ClientA.ClientParams = lightclient.NewRawParams([]byte("extra: true\n")) - require.Error(t, config.ValidateClients(nil)) + require.ErrorContains(t, config.ValidateClients(nil), "attestation clients take no params") }) } @@ -116,7 +128,7 @@ func TestValidateClients(t *testing.T) { // 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 := proofgen.NewRawParams([]byte("proverURL: https://example.com\n")) + params := lightclient.NewRawParams([]byte("proverURL: https://example.com\n")) var p stubParams require.ErrorContains(t, params.Decode(&p), "proverURL") @@ -124,7 +136,7 @@ func TestRawParamsRejectsUnknownField(t *testing.T) { } func TestRawParamsRoundTrip(t *testing.T) { - params := proofgen.NewRawParams([]byte("proverUrl: https://example.com\n")) + params := lightclient.NewRawParams([]byte("proverUrl: https://example.com\n")) var p stubParams require.NoError(t, params.Decode(&p)) @@ -132,7 +144,7 @@ func TestRawParamsRoundTrip(t *testing.T) { } func TestRawParamsEmptyDecodeIsNoOp(t *testing.T) { - var params *proofgen.RawParams + var params *lightclient.RawParams var p stubParams require.NoError(t, params.Decode(&p)) diff --git a/link/internal/config/relayer.go b/link/internal/config/relayer.go index 7d2c28dc8..84924602d 100644 --- a/link/internal/config/relayer.go +++ b/link/internal/config/relayer.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" - "github.com/cosmos/ibc/link/proofgen" + "github.com/cosmos/ibc/link/lightclient" ) // ClientType the light client type. @@ -69,7 +69,7 @@ type ClientEnd struct { // Params is the client-type-specific config block, captured verbatim and // interpreted by the proof generator factory registered for Type. A // pointer so that adding it keeps ClientEnd comparable. - Params *proofgen.RawParams `yaml:"params,omitempty"` + ClientParams *lightclient.RawParams `yaml:"clientParams,omitempty"` // AutoRelay configures auto-relay for packets flowing FROM this end's // chain TOWARD the counterparty end. diff --git a/link/internal/livevalidate/quorum.go b/link/internal/livevalidate/quorum.go index 262ceed6c..4d5e44734 100644 --- a/link/internal/livevalidate/quorum.go +++ b/link/internal/livevalidate/quorum.go @@ -8,7 +8,6 @@ import ( "github.com/pkg/errors" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/clienttypes" "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/service/attestor" @@ -34,16 +33,11 @@ func checkAttestorQuorum(ctx context.Context, cfg config.Config, clientSet *chai attestors = append(attestors, local...) attestors = append(attestors, remote...) - reg, err := clienttypes.Builtins(clientSet, attestors, nil) - if err != nil { - return errors.Wrap(err, "client types") - } - - if err := cfg.ValidateClients(reg); err != nil { + if err := cfg.ValidateClients(nil); err != nil { return errors.Wrap(err, "client config") } - if _, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, reg); err != nil { + if _, err := proofgen.NewSetFromConfig(ctx, cfg, clientSet, attestors, nil); err != nil { return errors.Wrap(err, "attestor quorum") } diff --git a/link/internal/relay/processors/batch_recv_packet_test.go b/link/internal/relay/processors/batch_recv_packet_test.go index bf16b9891..086937cb1 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 @@ -104,7 +105,7 @@ func TestBatchRecvPacketSequenceAlignment(t *testing.T) { proofGen := mocks.NewMockProofGenerator(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) @@ -201,7 +202,7 @@ func TestBatchRecvPacketToleratesPartialEventFetchFailure(t *testing.T) { proofGen := mocks.NewMockProofGenerator(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) @@ -306,7 +307,7 @@ func TestBatchRecvPacketExcludesNotYetProvablePackets(t *testing.T) { proofGen := mocks.NewMockProofGenerator(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..39c4672ff 100644 --- a/link/internal/relay/processors/batch_relay.go +++ b/link/internal/relay/processors/batch_relay.go @@ -14,6 +14,7 @@ import ( "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 +53,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 } } diff --git a/link/internal/relay/proofgen/attestation/factory.go b/link/internal/relay/proofgen/attestation/factory.go deleted file mode 100644 index eab5f3661..000000000 --- a/link/internal/relay/proofgen/attestation/factory.go +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package attestation - -import ( - "context" - - "github.com/pkg/errors" - - "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" - "github.com/cosmos/ibc/link/internal/service/attestor" - pgen "github.com/cosmos/ibc/link/proofgen" -) - -// ClientType is the config `type:` name this factory registers under. -const ClientType = string(config.ClientTypeAttestation) - -var ( - _ pgen.Factory = factory{} - _ pgen.ProofGenerator = (*Generator)(nil) -) - -// factory builds attestation proof generators. It closes over the attestor set -// and chain clients it needs, so the generic registry never carries -// attestation-specific arguments. -type factory struct { - clientSet *chains.ClientSet - attestors []attestor.Attestor -} - -// NewFactory returns the built-in attestation client factory. attestors is -// every attestor this process can reach, local and remote; which of them back -// a given client is resolved per client end against on-chain state. -func NewFactory(clientSet *chains.ClientSet, attestors []attestor.Attestor) pgen.Factory { - return factory{clientSet: clientSet, attestors: attestors} -} - -// ValidateParams rejects params outright: the attestation client is configured -// through the top-level `attestors` block and its on-chain attestation set, not -// through per-client params. -func (f factory) ValidateParams(params *pgen.RawParams) error { - if !params.IsEmpty() { - return errors.New("attestation clients take no params; configure attestors under .attestors") - } - - return nil -} - -// New resolves the attestor quorum for self and builds its generator. Deps are -// unused: this factory already holds richer chain access than the public -// CounterpartyChain view exposes, because it must query self's on-chain -// attestation set. -func (f factory) New( - ctx context.Context, - _ pgen.Deps, - self, counterparty pgen.ClientEnd, -) (pgen.ProofGenerator, error) { - return ResolveGenerator(ctx, toConfigEnd(self), toConfigEnd(counterparty), f.clientSet, f.attestors) -} - -func toConfigEnd(end pgen.ClientEnd) config.ClientEnd { - return config.ClientEnd{ - ChainID: end.ChainID, - ClientID: end.ClientID, - Type: config.ClientType(end.Type), - } -} diff --git a/link/internal/relay/proofgen/attestation/generator.go b/link/internal/relay/proofgen/attestation/generator.go index 3604d3932..11fccc4da 100644 --- a/link/internal/relay/proofgen/attestation/generator.go +++ b/link/internal/relay/proofgen/attestation/generator.go @@ -13,10 +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 +// Generator implements lightclient.ProofGenerator for one configured // attestation light client: LatestProvableHeight/StateProof/PacketProofs all // query the same fixed attestor set with the same quorum threshold type Generator struct { @@ -63,7 +63,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 +124,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 8712b903e..a5f615141 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -12,15 +12,15 @@ import ( "github.com/cosmos/ibc/link/internal/chains" "github.com/cosmos/ibc/link/internal/config" - pgen "github.com/cosmos/ibc/link/proofgen" + "github.com/cosmos/ibc/link/internal/relay/proofgen/attestation" + 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. -// -// Defined in the public proofgen package so custom light clients can implement -// it; aliased here so internal callers are unaffected. -type ProofGenerator = pgen.ProofGenerator +// ProofGenerator is the public light-client proof generation contract. +// Keep the alias here so existing internal relay code can continue to refer to +// the package that owns generator resolution and lookup. +type ProofGenerator = lightclient.ProofGenerator // Key identifies one configured light client by the chain it lives on and // its client id, the composite key ProofGenerator instances are scoped by. @@ -47,18 +47,19 @@ func (s *Set) Get(chainID, clientID string) (ProofGenerator, bool) { } // NewSetFromConfig resolves a ProofGenerator for every client end of every -// configured connection, dispatching to the factory registered for each end's -// client type. +// configured connection. Attestation uses its built-in resolver; other client +// types dispatch through the caller-supplied registry. func NewSetFromConfig( ctx context.Context, cfg config.Config, clientSet *chains.ClientSet, - reg *pgen.Registry, + attestors []attestorservice.Attestor, + reg *lightclient.Registry, ) (*Set, error) { generators := make(map[string]ProofGenerator, len(cfg.Relayer.Connections)*2) err := forEachClientEnd(cfg, func(connAlias string, self, counterparty config.ClientEnd) error { - return addGenerator(ctx, generators, connAlias, self, counterparty, clientSet, reg) + return addGenerator(ctx, generators, connAlias, self, counterparty, clientSet, attestors, reg) }) if err != nil { return nil, err @@ -92,29 +93,35 @@ func addGenerator( connAlias string, client, clientCounterparty config.ClientEnd, clientSet *chains.ClientSet, - reg *pgen.Registry, + attestors []attestorservice.Attestor, + reg *lightclient.Registry, ) error { - factory, ok := reg.Get(string(client.Type)) - if !ok { - return errors.Errorf( - "connection %q: no proof generator registered for client type %q (registered: %v)", - connAlias, client.Type, reg.Types(), + if client.Type == config.ClientTypeAttestation { + gen, err := attestation.ResolveGenerator( + ctx, client, clientCounterparty, clientSet, attestors, ) + if err != nil { + return err + } + + generators[Key(client.ChainID, client.ClientID)] = gen + + return nil } - counterpartyChain, ok := clientSet.Get(clientCounterparty.ChainID) + factory, ok := reg.Get(string(client.Type)) if !ok { return errors.Errorf( - "connection %q: no configured chain client for counterparty chain %q", - connAlias, clientCounterparty.ChainID, + "connection %q: no proof generator registered for client type %q (registered: %v)", + connAlias, client.Type, reg.Types(), ) } gen, err := factory.New( ctx, - pgen.Deps{Counterparty: counterpartyChain}, - toPublicEnd(client), - toPublicEnd(clientCounterparty), + lightclient.FactoryOptions{ + Client: toClientInfo(client, clientCounterparty.ChainID), + }, ) if err != nil { return errors.Wrapf(err, "connection %q", connAlias) @@ -125,11 +132,12 @@ func addGenerator( return nil } -func toPublicEnd(end config.ClientEnd) pgen.ClientEnd { - return pgen.ClientEnd{ - ChainID: end.ChainID, - ClientID: end.ClientID, - Type: string(end.Type), - Params: end.Params, +func toClientInfo(end config.ClientEnd, counterpartyChainID string) lightclient.ClientInfo { + return lightclient.ClientInfo{ + ChainID: end.ChainID, + CounterpartyChainID: counterpartyChainID, + 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 2177c1de6..68bbb1a07 100644 --- a/link/internal/relay/proofgen/proofgen_test.go +++ b/link/internal/relay/proofgen/proofgen_test.go @@ -10,10 +10,9 @@ import ( "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" "github.com/cosmos/ibc/link/internal/tests/mocks" - pgen "github.com/cosmos/ibc/link/proofgen" + "github.com/cosmos/ibc/link/lightclient" ) func testConnection() config.ConnectionConfig { @@ -87,7 +86,7 @@ func TestNewSetFromConfig(t *testing.T) { cfg, clientSet, attestors := testConfig(t) conn := cfg.Relayer.Connections[0] - set, err := NewSetFromConfig(ctx, cfg, clientSet, testRegistry(t, clientSet, attestors)) + set, err := NewSetFromConfig(ctx, cfg, clientSet, attestors, nil) require.NoError(t, err) _, ok := set.Get(conn.ClientA.ChainID, conn.ClientA.ClientID) @@ -111,7 +110,7 @@ func TestNewSetFromConfig(t *testing.T) { cfg := config.Config{Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}} // ACT - _, err := NewSetFromConfig(ctx, cfg, clientSet, testRegistry(t, clientSet, nil)) + _, err := NewSetFromConfig(ctx, cfg, clientSet, nil, nil) // ASSERT require.ErrorContains(t, err, `no proof generator registered for client type "tendermint"`) @@ -132,10 +131,10 @@ func TestNewSetFromConfig(t *testing.T) { cfg := config.Config{Relayer: config.RelayerConfig{Connections: []config.ConnectionConfig{conn}}} - reg := pgen.NewRegistry() + reg := lightclient.NewRegistry() require.NoError(t, reg.Register("myclient", stubFactory{})) - set, err := NewSetFromConfig(ctx, cfg, clientSet, reg) + set, err := NewSetFromConfig(ctx, cfg, clientSet, nil, reg) require.NoError(t, err) _, ok := set.Get(conn.ClientA.ChainID, conn.ClientA.ClientID) @@ -143,26 +142,15 @@ func TestNewSetFromConfig(t *testing.T) { }) } -// testRegistry builds the built-in registry the relayer normally assembles at -// startup. -func testRegistry(t *testing.T, clientSet *chains.ClientSet, attestors []attestor.Attestor) *pgen.Registry { - t.Helper() - - reg := pgen.NewRegistry() - require.NoError(t, reg.Register(attestation.ClientType, attestation.NewFactory(clientSet, attestors))) - - return reg -} - // stubFactory is a light client type that exists only in this test. type stubFactory struct{} -func (stubFactory) ValidateParams(*pgen.RawParams) error { return nil } +func (stubFactory) ValidateParams(*lightclient.RawParams) error { return nil } func (stubFactory) New( - context.Context, pgen.Deps, pgen.ClientEnd, pgen.ClientEnd, -) (pgen.ProofGenerator, error) { + context.Context, lightclient.FactoryOptions, +) (lightclient.ProofGenerator, error) { return stubGenerator{}, nil } -type stubGenerator struct{ pgen.ProofGenerator } +type stubGenerator struct{ lightclient.ProofGenerator } diff --git a/link/internal/relay/txbuilder/evm/evm.go b/link/internal/relay/txbuilder/evm/evm.go index 062a6e8f6..81a5646ed 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 the already-encoded proof produced by lightclient.ProofGenerator.StateProof. 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..94cfcc2bd 100644 --- a/link/internal/tests/mocks/proofgen.go +++ b/link/internal/tests/mocks/proofgen.go @@ -7,7 +7,7 @@ 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" ) @@ -106,7 +106,7 @@ func (_c *MockProofGenerator_LatestProvableHeight_Call) RunAndReturn(run func(ct } // 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) { +func (_mock *MockProofGenerator) 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) @@ -141,13 +141,13 @@ type MockProofGenerator_PacketProofs_Call struct { // 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 (_c *MockProofGenerator_PacketProofs_Call) Run(run func(ctx context.Context, height uint64, kind v2.ProofKind, packets []types.Packet)) *MockProofGenerator_PacketProofs_Call { +func (_c *MockProofGenerator_PacketProofs_Call) Run(run func(ctx context.Context, height uint64, kind lightclient.ProofKind, packets []types.Packet)) *MockProofGenerator_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 { @@ -180,7 +180,7 @@ func (_c *MockProofGenerator_PacketProofs_Call) Return(bytess [][]byte, err erro 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 *MockProofGenerator_PacketProofs_Call) RunAndReturn(run func(ctx context.Context, height uint64, kind lightclient.ProofKind, packets []types.Packet) ([][]byte, error)) *MockProofGenerator_PacketProofs_Call { _c.Call.Return(run) return _c } diff --git a/link/internal/types/v2/client.go b/link/internal/types/v2/client.go index 824ecc612..fdb11d997 100644 --- a/link/internal/types/v2/client.go +++ b/link/internal/types/v2/client.go @@ -4,16 +4,15 @@ package v2 import ( "math" - - "github.com/cosmos/ibc/link/proofgen" + "time" ) // BlockHeader is the minimal view of a block the attestor and proof generators // need: a height and its timestamp. -// -// Defined in the public proofgen package so custom light clients can implement -// against it; aliased here so internal callers are unaffected. -type BlockHeader = proofgen.BlockHeader +type BlockHeader struct { + Height uint64 + Timestamp time.Time +} // Special markers for different block heights. const ( diff --git a/link/internal/types/v2/types.go b/link/internal/types/v2/types.go index a82f16197..4925e6db4 100644 --- a/link/internal/types/v2/types.go +++ b/link/internal/types/v2/types.go @@ -7,8 +7,6 @@ import ( "time" channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" - - "github.com/cosmos/ibc/link/proofgen" ) // EventKind the kind of packet event. @@ -31,20 +29,6 @@ const ( WriteAckStatusError ) -// ProofKind the kind of packet claim a proof attests to. -// -// Defined in the public proofgen package so custom light clients can implement -// against it; aliased here so internal callers are unaffected. -type ProofKind = proofgen.ProofKind - -// Proof kinds -const ( - ProofKindUnknown = proofgen.ProofKindUnknown - ProofKindPacketCommitment = proofgen.ProofKindPacketCommitment - ProofKindAcknowledgement = proofgen.ProofKindAcknowledgement - ProofKindReceiptAbsence = proofgen.ProofKindReceiptAbsence -) - // RelayKind the packet operation one PacketRelayItem asks to perform. type RelayKind int diff --git a/link/proofgen/params.go b/link/lightclient/params.go similarity index 95% rename from link/proofgen/params.go rename to link/lightclient/params.go index a83a38333..aa549b043 100644 --- a/link/proofgen/params.go +++ b/link/lightclient/params.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package proofgen +package lightclient import ( "github.com/goccy/go-yaml" @@ -26,7 +26,7 @@ func NewRawParams(raw []byte) *RawParams { // UnmarshalYAML captures the params block without interpreting it. func (p *RawParams) UnmarshalYAML(b []byte) error { if p == nil { - return errors.New("proofgen: UnmarshalYAML on nil RawParams") + return errors.New("lightclient: UnmarshalYAML on nil RawParams") } p.raw = append([]byte(nil), b...) diff --git a/link/proofgen/proofgen.go b/link/lightclient/proofgen.go similarity index 55% rename from link/proofgen/proofgen.go rename to link/lightclient/proofgen.go index 330460d00..aa727c238 100644 --- a/link/proofgen/proofgen.go +++ b/link/lightclient/proofgen.go @@ -1,13 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 -// Package proofgen is the public extension point for light client proof +// Package lightclient is the public extension point for light client proof // generation. Implement ProofGenerator and Factory here, register the factory -// under a client type name, and start a relayer with it via link/app. +// under a client type name, and supply it through link/cli. // // This package must not import anything under link/internal. Everything it // exposes is deliberately narrow: proofs are opaque bytes, and the relayer // never interprets them. -package proofgen +package lightclient import ( "context" @@ -51,37 +51,19 @@ const ( ProofKindReceiptAbsence ) -// BlockHeader is the minimal view of a counterparty block a proof generator -// needs to reason about heights and timestamps. -type BlockHeader struct { - Height uint64 - Timestamp time.Time +// ClientInfo describes one configured light-client instance from the +// perspective of the Factory responsible for its type. +type ClientInfo struct { + ChainID string + CounterpartyChainID string + ClientID string + Type string + ClientParams *RawParams } -// CounterpartyChain reads state from the chain a light client tracks. It is a -// deliberately narrow view of the relayer's full chain client; it may gain -// methods over time, which is safe because implementations live in the relayer -// and generators only consume it. -type CounterpartyChain interface { - ChainID() string - - // GetBlockHeader returns the header at height. The special height markers - // used internally by the relayer are not part of this contract; pass a - // concrete height. - GetBlockHeader(ctx context.Context, height uint64) (BlockHeader, error) -} - -// Deps are the relayer-provided dependencies handed to a Factory. -type Deps struct { - // Counterparty reads the chain this client tracks. - Counterparty CounterpartyChain -} - -// ClientEnd is one side of a configured connection: a light client on ChainID, -// tracking the connection's other end as its counterparty. -type ClientEnd struct { - ChainID string - ClientID string - Type string - Params *RawParams +// FactoryOptions describes the configured light-client instance a Factory +// constructs. Additional shared construction dependencies may be added here +// as the extension API evolves. +type FactoryOptions struct { + Client ClientInfo } diff --git a/link/proofgen/registry.go b/link/lightclient/registry.go similarity index 71% rename from link/proofgen/registry.go rename to link/lightclient/registry.go index dc260cefc..df4a836f6 100644 --- a/link/proofgen/registry.go +++ b/link/lightclient/registry.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package proofgen +package lightclient import ( "context" @@ -18,13 +18,13 @@ type Factory interface { // rejected. ValidateParams(params *RawParams) error - // New builds a generator for self, which tracks counterparty. Params have - // already passed ValidateParams. - New(ctx context.Context, deps Deps, self, counterparty ClientEnd) (ProofGenerator, error) + // New builds a generator for Client, which tracks Counterparty. ClientParams + // have already passed ValidateParams. + New(ctx context.Context, options FactoryOptions) (ProofGenerator, error) } -// Registry resolves a Factory by client type name. The relayer registers its -// built-in types; callers add their own before starting a relayer. +// Registry resolves custom light-client factories by config type name. +// Built-in client types are resolved internally by the relayer. // // A Registry is not safe for concurrent registration. Build it fully during // startup, then treat it as read-only. @@ -66,22 +66,6 @@ func (r *Registry) Get(clientType string) (Factory, bool) { return f, ok } -// Merge registers every entry of other into r, failing on any name r already -// has. Used to fold caller-supplied custom types into the relayer's built-ins. -func (r *Registry) Merge(other *Registry) error { - if other == nil { - return nil - } - - for name, f := range other.factories { - if err := r.Register(name, f); err != nil { - return err - } - } - - return nil -} - // Types lists every registered client type, sorted. Useful for error messages. func (r *Registry) Types() []string { if r == nil { From f393056a8c366b7408398df37afbceda7e70d5d6 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 14:47:15 -0700 Subject: [PATCH 03/17] feat(link): add remote light client proof of concept Signed-off-by: Dennis Fang --- .../remotepoc/attestation_handler.go | 90 ++++++++++++ link/lightclient/remotepoc/remote.go | 133 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 link/lightclient/remotepoc/attestation_handler.go create mode 100644 link/lightclient/remotepoc/remote.go diff --git a/link/lightclient/remotepoc/attestation_handler.go b/link/lightclient/remotepoc/attestation_handler.go new file mode 100644 index 000000000..c49f55d74 --- /dev/null +++ b/link/lightclient/remotepoc/attestation_handler.go @@ -0,0 +1,90 @@ +// 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 exposes generator through the proof-of-concept HTTP protocol. +func NewHandler(generator lightclient.ProofGenerator) *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 = generator.LatestProvableHeight(r.Context()) + case "state": + res.Proof, err = generator.StateProof(r.Context(), req.Height) + case "packets": + var proofs [][]byte + proofs, err = generator.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 constructs the configured attestation generator for +// one client and exposes it through the remote light-client HTTP protocol. +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) + } + generator, err := attestation.ResolveGenerator( + ctx, self, counterparty, clients, append(local, remoteAttestors...), + ) + if err != nil { + return nil, errors.Wrap(err, "resolve attestation proof generator") + } + + return NewHandler(generator), nil +} diff --git a/link/lightclient/remotepoc/remote.go b/link/lightclient/remotepoc/remote.go new file mode 100644 index 000000000..cd0b7abfc --- /dev/null +++ b/link/lightclient/remotepoc/remote.go @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package remotepoc adapts a light-client proof generator to a small HTTP/JSON +// protocol. It is a proof of concept, not a production transport. +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 config name of the remote proof generator. +const Type = "remote" + +// ClientParams configure the remote proof service for one light client. +type ClientParams struct { + URL string `yaml:"url"` +} + +// Factory constructs remote proof generators. +type Factory struct { + HTTPClient *http.Client +} + +func (Factory) ValidateParams(raw *lightclient.RawParams) error { + var p ClientParams + if err := raw.Decode(&p); err != nil { + return err + } + + u, err := url.ParseRequestURI(p.URL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return errors.New("url must be an absolute HTTP(S) URL") + } + + return nil +} + +func (f Factory) New( + _ context.Context, + options lightclient.FactoryOptions, +) (lightclient.ProofGenerator, error) { + var p ClientParams + if err := options.Client.ClientParams.Decode(&p); err != nil { + return nil, err + } + + client := f.HTTPClient + if client == nil { + client = &http.Client{Timeout: 30 * time.Second} + } + + return &Generator{url: strings.TrimRight(p.URL, "/") + "/proof", client: client}, nil +} + +// Generator forwards proof generation to an HTTP service. +type Generator 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 (g *Generator) LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) { + res, err := g.call(ctx, request{Operation: "latest"}) + return res.Height, res.Timestamp, err +} + +func (g *Generator) StateProof(ctx context.Context, height uint64) ([]byte, error) { + res, err := g.call(ctx, request{Operation: "state", Height: height}) + return res.Proof, err +} + +func (g *Generator) PacketProofs( + ctx context.Context, + height uint64, + kind lightclient.ProofKind, + packets []channeltypesv2.Packet, +) ([][]byte, error) { + res, err := g.call(ctx, request{Operation: "packets", Height: height, Kind: kind, Packets: packets}) + return res.Proofs, err +} + +func (g *Generator) 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, g.url, strings.NewReader(string(body))) + if err != nil { + return response{}, err + } + httpReq.Header.Set("Content-Type", "application/json") + + httpRes, err := g.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 +} From 8a59a76450201dee2d411fdb51bcb8225f0a84f4 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 14:47:22 -0700 Subject: [PATCH 04/17] test(e2e): relay with a remote custom light client Signed-off-by: Dennis Fang --- e2e/cmd/custom-ibc/main.go | 27 ++ e2e/custom_light_client_cli_test.go | 160 ++++++++++ e2e/go.mod | 2 +- e2e/inprocess_proofgen_test.go | 301 ------------------ .../customlightclient/customlightclient.go | 71 +++++ .../harness/ibclink/relayer_config.go | 47 ++- 6 files changed, 290 insertions(+), 318 deletions(-) create mode 100644 e2e/cmd/custom-ibc/main.go create mode 100644 e2e/custom_light_client_cli_test.go delete mode 100644 e2e/inprocess_proofgen_test.go create mode 100644 e2e/internal/customlightclient/customlightclient.go diff --git a/e2e/cmd/custom-ibc/main.go b/e2e/cmd/custom-ibc/main.go new file mode 100644 index 000000000..9203b113b --- /dev/null +++ b/e2e/cmd/custom-ibc/main.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + + "github.com/cosmos/ibc/e2e/internal/customlightclient" + "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(customlightclient.Type, customlightclient.Factory{}); err != nil { + panic(err) + } + if err := registry.Register(remotepoc.Type, remotepoc.Factory{}); err != nil { + panic(err) + } + + root := cli.NewRootCmd(cli.Options{ + Relayer: cli.RelayerOptions{LightClients: 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..2b2fad3cb --- /dev/null +++ b/e2e/custom_light_client_cli_test.go @@ -0,0 +1,160 @@ +// 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/customlightclient" + "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" +) + +// TestCustomCompiledCLILoadsLightClient proves that a downstream binary can +// register a light-client implementation and select it through the ordinary +// `ibc relayer run` path. +func TestCustomCompiledCLILoadsLightClient(t *testing.T) { + binary := buildCustomIBC(t) + t.Setenv("IBC_BIN", binary) + + spec, runtime := attestedMesh(e2etest.EVMChains( + t, e2etest.EVMRequirements{}, e2etest.ChainA, e2etest.ChainB, + )) + env := e2etest.Start(t, spec, runtime) + + marker := filepath.Join(t.TempDir(), "custom-client-created") + route := e2etest.AtoB(e2etest.ChainA, e2etest.ChainB) + driver, _ := e2etest.DeployWithRelayerConfig( + t, + env, + e2etest.NewSigner(t), + e2etest.NewSigner(t), + func(cfg *ibclink.RelayerConfig) { + require.NotEmpty(t, cfg.Connections) + cfg.Connections[0].ClientAType = customlightclient.Type + cfg.Connections[0].ClientAParams = map[string]any{"markerFile": marker} + }, + route, + ) + + relayer := e2etest.StartRelayer(t, driver, env) + require.NotEmpty(t, relayer.Ready().HTTP) + + createdFor, err := os.ReadFile(marker) + require.NoError(t, err, "custom light-client factory was not invoked") + require.NotEmpty(t, createdFor) +} + +// TestRemoteAttestationLightClientRelaysPacket proves a custom-compiled CLI can +// complete a packet relay using the built-in attestation generator through the +// remote light-client HTTP adapter. +func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { + binary := buildCustomIBC(t) + t.Setenv("IBC_BIN", binary) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { _ = listener.Close() }) + remoteURL := "http://" + listener.Addr().String() + + 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 + 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": remoteURL} + }, + route, + ) + + useEnvironmentRPCs(t, env, &serviceConfig) + serviceConfigPath := filepath.Join(t.TempDir(), "attestation-proof-service.yaml") + require.NoError(t, ibclink.WriteRelayerConfig(serviceConfigPath, serviceConfig)) + remoteEnd := serviceConfig.Connections[0] + server, err := remotepoc.NewAttestationHandler( + t.Context(), serviceConfigPath, remoteEnd.ChainA, remoteEnd.ClientA, + ) + require.NoError(t, err) + serverErrors := make(chan error, 1) + go func() { serverErrors <- server.Serve(listener) }() + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, server.Shutdown(ctx)) + serveErr := <-serverErrors + require.ErrorIs(t, serveErr, http.ErrServerClosed) + }) + + relayer := e2etest.StartRelayer(t, driver, env) + transfer, err := e2etest.NewTransfer(t, env, deployment, sender, route).Send( + t.Context(), e2etest.TransferRequest{Amount: big.NewInt(1_234_000)}, + ) + require.NoError(t, err) + require.NoError(t, e2etest.RelayAll(t.Context(), relayer, transfer.PacketTx())) + _, err = e2etest.AwaitState( + t.Context(), relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED, + ) + require.NoError(t, err) + require.NoError(t, transfer.VerifyDelivered(t.Context())) +} + +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 be35c9e6e..0d8e4eaa6 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -366,7 +366,7 @@ require ( replace github.com/cosmos/ibc/link => ../link // Mirrors link/go.mod. Replace directives are not inherited from dependency -// modules, so importing link/app — which compiles link's signer stack — makes +// modules, so importing link/cli — which compiles link's signer stack — makes // this module resolve stock cometbft, which lacks privval.ParseNoiseAddr and // breaks github.com/cosmos/kms/config. replace github.com/cometbft/cometbft => github.com/cometbft/cometbft v0.39.0-rc1.0.20260615134937-9ea34470f336 diff --git a/e2e/inprocess_proofgen_test.go b/e2e/inprocess_proofgen_test.go deleted file mode 100644 index 6a3f87590..000000000 --- a/e2e/inprocess_proofgen_test.go +++ /dev/null @@ -1,301 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package e2e_test - -import ( - "context" - "fmt" - "math/big" - "net/http" - "os" - "path/filepath" - "strconv" - "testing" - "time" - - "connectrpc.com/connect" - "github.com/ethereum/go-ethereum/crypto" - "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/app" - "github.com/cosmos/ibc/link/keyfile" - "github.com/cosmos/ibc/link/proofgen" -) - -// TestInProcessRelayerWithRegisteredProofGenerator is a proof of concept for -// the pluggable proof generator work. It proves three things the existing -// suite cannot: -// -// 1. A relayer can be constructed and run entirely in-process through the -// public link/app entrypoint, rather than by executing the ibc binary. -// The harness daemon shells out, so there is no way to hand it a registry. -// 2. Attestors run inside that same process (dual mode), with no separate -// attestor daemons. -// 3. A caller-registered light client type reaches proof generation. The -// attestation client is resolved through the same registry, so relaying -// working at all is evidence the switch-to-registry port is sound. -// -// It deliberately does not use e2etest.StartRelayer or AwaitState, both of -// which require the daemon handle. No harness files are modified. -func TestInProcessRelayerWithRegisteredProofGenerator(t *testing.T) { - t.Parallel() - - chainSpecs := e2etest.EVMChains(t, e2etest.EVMRequirements{}, e2etest.ChainA, e2etest.ChainB) - spec, runtime := attestedMesh(chainSpecs) - env := e2etest.Start(t, spec, runtime) - - sender := e2etest.NewSigner(t) - relayerSigner := e2etest.NewSigner(t) - route := e2etest.AtoB(e2etest.ChainA, e2etest.ChainB) - - // Capture the config the harness builds, rewriting its attestors from - // separate gRPC daemons to in-process local ones backed by the same keys - // the environment baked into each client's on-chain attestation set. - var captured ibclink.RelayerConfig - - keyDir := t.TempDir() - - driver, deployment := e2etest.DeployWithRelayerConfig( - t, - env, - sender, - relayerSigner, - func(config *ibclink.RelayerConfig) { - config.Attestors = localAttestors(t, env, runtime, keyDir) - captured = *config - }, - route, - ) - require.NotNil(t, driver) - - // The harness writes chain RPCs as ${IBC_LINK_CHAIN_RPC_...} placeholders - // and injects the values into the environment of the subprocess it spawns. - // Running in-process there is no such subprocess, so substitute the real - // endpoints. - for i := range captured.Chains { - captured.Chains[i].RPC = rpcURLForEVMChainID(t, env, captured.Chains[i].ChainID) - } - - // The in-process relayer gets its own config file and database so it never - // contends with the harness driver's. - runDir := t.TempDir() - captured.DBPath = filepath.Join(runDir, "inprocess.db") - configPath := filepath.Join(runDir, "ibc-link.config.yaml") - require.NoError(t, ibclink.WriteRelayerConfig(configPath, captured), "write in-process config") - - // A client type that exists only in this test, proving the registry - // accepts arbitrary names from outside the relayer's own packages. - registry := proofgen.NewRegistry() - require.NoError(t, registry.Register("poc-custom-client", pocFactory{})) - - relayer, err := app.New(t.Context(), app.Options{ - ConfigPath: configPath, - Registry: registry, - }) - require.NoError(t, err, "construct relayer in-process") - - address, err := relayer.Start(t.Context()) - require.NoError(t, err, "start relayer") - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - require.NoError(t, relayer.Stop(ctx), "stop relayer") - }) - - require.ElementsMatch(t, evmChainIDs(t, env), relayer.ChainIDs(), "relayer connected to both chains") - - client := relayerv2.NewRelayerApiServiceClient( - &http.Client{Timeout: 10 * time.Second}, - "http://"+address.String(), - connect.WithGRPC(), - ) - - ctx := t.Context() - - iftApp := e2etest.NewIFT(t, env, deployment, sender, route) - transfer, err := iftApp.Send(ctx, e2etest.IFTRequest{Amount: big.NewInt(1_234_000)}) - require.NoError(t, err, "send transfer") - - packet := transfer.PacketTx() - sourceChainID := evmChainID(t, env, packet.Source) - - _, err = client.Relay(ctx, connect.NewRequest(&relayerv2.RelayRequest{ - TxHash: packet.SourceTxHash, - SourceChainId: sourceChainID, - Selection: &relayerv2.RelayRequest_AllPackets{AllPackets: &relayerv2.AllPackets{}}, - })) - require.NoError(t, err, "trigger relay") - - status := awaitSucceeded(ctx, t, client, packet.SourceTxHash, sourceChainID) - require.NotEmpty(t, status.GetRecvTx().GetTxHash(), "recv tx recorded") - require.NotEmpty(t, status.GetAckTx().GetTxHash(), "ack tx recorded") - - require.NoError(t, transfer.VerifyDelivered(ctx)) - require.NoError(t, transfer.VerifyBurned(ctx), "a successful ack must not also refund") -} - -// awaitSucceeded polls the relayer's own status API until the packet reports -// success. A local reimplementation of e2etest.AwaitState, which needs the -// daemon handle this test does not have. -func awaitSucceeded( - ctx context.Context, - t *testing.T, - client relayerv2.RelayerApiServiceClient, - txHash, sourceChainID string, -) *relayerv2.PacketStatus { - t.Helper() - - deadline := time.Now().Add(90 * time.Second) - - var last relayerv2.PacketState - - for time.Now().Before(deadline) { - res, err := client.Status(ctx, connect.NewRequest(&relayerv2.StatusRequest{ - TxHash: txHash, SourceChainId: sourceChainID, - })) - if err == nil { - for _, packet := range res.Msg.GetPacketStatuses() { - last = packet.GetState() - if last == relayerv2.PacketState_PACKET_STATE_SUCCEEDED { - return packet - } - - require.NotEqual( - t, - relayerv2.PacketState_PACKET_STATE_RELAY_FAILED, - last, - "packet permanently failed to relay", - ) - } - } - - select { - case <-ctx.Done(): - t.Fatalf("context cancelled awaiting relay: %v", ctx.Err()) - case <-time.After(500 * time.Millisecond): - } - } - - t.Fatalf("packet did not succeed within budget; last observed state %v", last) - - return nil -} - -// localAttestors converts the environment's attestors into in-process entries. -// Each attestor watches the chain on the far side of the client it backs, and -// signs with the authority key the environment registered in that client's -// on-chain attestation set. -func localAttestors( - t *testing.T, - env *environment.Environment, - runtime environment.Runtime, - keyDir string, -) []ibclink.RelayerAttestor { - t.Helper() - - chains := env.Chains() - require.Len(t, chains, 2, "poc assumes a two chain mesh") - - attestors := make([]ibclink.RelayerAttestor, 0, 2) - - for _, host := range chains { - counterparty := chains[0] - if counterparty == host { - counterparty = chains[1] - } - - // The attestor backing the client on host watches counterparty. - id := meshAttestorFor(host, counterparty) - authority, ok := runtime.Authorities[environment.AuthorityID(id)] - require.True(t, ok, "no authority for attestor %q", id) - - attestors = append(attestors, ibclink.RelayerAttestor{ - Name: string(id), - Type: ibclink.RelayerAttestorLocal, - ChainID: evmChainID(t, env, counterparty), - KeyFile: writeKeyFile(t, keyDir, string(id), authority.PrivateKeyHex), - }) - } - - return attestors -} - -func writeKeyFile(t *testing.T, dir, name, privateKeyHex string) string { - t.Helper() - - require.NoError(t, os.MkdirAll(dir, 0o700)) - - key, err := crypto.HexToECDSA(privateKeyHex) - require.NoError(t, err, "parse attestor key %q", name) - - path := filepath.Join(dir, name+".json") - require.NoError(t, keyfile.Store(path, keyfile.ECDSA, crypto.FromECDSA(key)), "store attestor key") - - return path -} - -// rpcURLForEVMChainID resolves the live RPC endpoint of the chain whose EVM -// chain id matches evmID. -func rpcURLForEVMChainID(t *testing.T, env *environment.Environment, evmID string) string { - t.Helper() - - for _, id := range env.Chains() { - chain, err := env.Chain(id) - require.NoError(t, err, "resolve chain %q", id) - - if strconv.FormatUint(chain.EVMChainID(), 10) == evmID { - return chain.RPCURL() - } - } - - t.Fatalf("no chain with EVM chain id %s", evmID) - - return "" -} - -func evmChainID(t *testing.T, env *environment.Environment, id environment.ChainID) string { - t.Helper() - - chain, err := env.Chain(id) - require.NoError(t, err, "resolve chain %q", id) - - return strconv.FormatUint(chain.EVMChainID(), 10) -} - -func evmChainIDs(t *testing.T, env *environment.Environment) []string { - t.Helper() - - ids := make([]string, 0, len(env.Chains())) - for _, id := range env.Chains() { - ids = append(ids, evmChainID(t, env, id)) - } - - return ids -} - -// pocFactory is a light client type defined entirely outside the relayer, -// registered only through app.Options.Registry. It is never selected by this -// test's config; its presence proves an arbitrary type name is accepted by -// config validation and generator resolution. -type pocFactory struct{} - -type pocParams struct { - ProverURL string `yaml:"proverUrl"` -} - -func (pocFactory) ValidateParams(params *proofgen.RawParams) error { - var p pocParams - - return params.Decode(&p) -} - -func (pocFactory) New( - context.Context, proofgen.Deps, proofgen.ClientEnd, proofgen.ClientEnd, -) (proofgen.ProofGenerator, error) { - return nil, fmt.Errorf("poc-custom-client generates no proofs") -} diff --git a/e2e/internal/customlightclient/customlightclient.go b/e2e/internal/customlightclient/customlightclient.go new file mode 100644 index 000000000..c653161b6 --- /dev/null +++ b/e2e/internal/customlightclient/customlightclient.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package customlightclient is an external light-client implementation used +// to validate custom-compiled IBC binaries. +package customlightclient + +import ( + "context" + "errors" + "os" + "time" + + channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" + + "github.com/cosmos/ibc/link/lightclient" +) + +const Type = "e2e-custom-client" + +type Factory struct{} + +type params struct { + MarkerFile string `yaml:"markerFile"` +} + +func (Factory) ValidateParams(raw *lightclient.RawParams) error { + var p params + if err := raw.Decode(&p); err != nil { + return err + } + if p.MarkerFile == "" { + return errors.New("markerFile is required") + } + + return nil +} + +func (Factory) New( + _ context.Context, + options lightclient.FactoryOptions, +) (lightclient.ProofGenerator, error) { + self := options.Client + var p params + if err := self.ClientParams.Decode(&p); err != nil { + return nil, err + } + if err := os.WriteFile(p.MarkerFile, []byte(self.ChainID+"/"+self.ClientID), 0o600); err != nil { + return nil, err + } + + return Generator{}, nil +} + +type Generator struct{} + +func (Generator) LatestProvableHeight(context.Context) (uint64, time.Time, error) { + return 0, time.Time{}, errors.New("e2e custom generator does not generate proofs") +} + +func (Generator) StateProof(context.Context, uint64) ([]byte, error) { + return nil, errors.New("e2e custom generator does not generate proofs") +} + +func (Generator) PacketProofs( + context.Context, + uint64, + lightclient.ProofKind, + []channeltypesv2.Packet, +) ([][]byte, error) { + return nil, errors.New("e2e custom generator does not generate proofs") +} 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"` } From cf2c8985d9830ecd6ec74dcd856d7cf0685b2ebb Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 17:14:21 -0700 Subject: [PATCH 05/17] refactor(link): simplify custom prover factories Signed-off-by: Dennis Fang --- e2e/cmd/custom-ibc/main.go | 4 +- .../customlightclient/customlightclient.go | 15 +- link/internal/bootstrap/bootstrap.go | 6 - link/internal/config/clients.go | 57 ------- link/internal/config/clients_test.go | 152 ------------------ link/internal/config/relayer_test.go | 5 +- link/internal/livevalidate/quorum.go | 4 - link/internal/livevalidate/quorum_test.go | 2 +- link/internal/relay/proofgen/proofgen.go | 36 ++++- link/internal/relay/proofgen/proofgen_test.go | 37 ++++- link/lightclient/params.go | 4 +- link/lightclient/params_test.go | 40 +++++ link/lightclient/proofgen.go | 23 ++- link/lightclient/registry.go | 43 +++-- link/lightclient/registry_test.go | 33 ++++ link/lightclient/remotepoc/remote.go | 18 +-- 16 files changed, 192 insertions(+), 287 deletions(-) delete mode 100644 link/internal/config/clients.go delete mode 100644 link/internal/config/clients_test.go create mode 100644 link/lightclient/params_test.go create mode 100644 link/lightclient/registry_test.go diff --git a/e2e/cmd/custom-ibc/main.go b/e2e/cmd/custom-ibc/main.go index 9203b113b..6a889425b 100644 --- a/e2e/cmd/custom-ibc/main.go +++ b/e2e/cmd/custom-ibc/main.go @@ -13,10 +13,10 @@ import ( func main() { registry := lightclient.NewRegistry() - if err := registry.Register(customlightclient.Type, customlightclient.Factory{}); err != nil { + if err := registry.Register(customlightclient.Factory{}); err != nil { panic(err) } - if err := registry.Register(remotepoc.Type, remotepoc.Factory{}); err != nil { + if err := registry.Register(remotepoc.Factory{}); err != nil { panic(err) } diff --git a/e2e/internal/customlightclient/customlightclient.go b/e2e/internal/customlightclient/customlightclient.go index c653161b6..2ad94a25b 100644 --- a/e2e/internal/customlightclient/customlightclient.go +++ b/e2e/internal/customlightclient/customlightclient.go @@ -23,17 +23,7 @@ type params struct { MarkerFile string `yaml:"markerFile"` } -func (Factory) ValidateParams(raw *lightclient.RawParams) error { - var p params - if err := raw.Decode(&p); err != nil { - return err - } - if p.MarkerFile == "" { - return errors.New("markerFile is required") - } - - return nil -} +func (Factory) Type() string { return Type } func (Factory) New( _ context.Context, @@ -44,6 +34,9 @@ func (Factory) New( if err := self.ClientParams.Decode(&p); err != nil { return nil, err } + if p.MarkerFile == "" { + return nil, errors.New("markerFile is required") + } if err := os.WriteFile(p.MarkerFile, []byte(self.ChainID+"/"+self.ClientID), 0o600); err != nil { return nil, err } diff --git a/link/internal/bootstrap/bootstrap.go b/link/internal/bootstrap/bootstrap.go index b4f914a00..fdb3b81e8 100644 --- a/link/internal/bootstrap/bootstrap.go +++ b/link/internal/bootstrap/bootstrap.go @@ -6,8 +6,6 @@ import ( "context" "log/slog" - "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/dispatch" @@ -88,10 +86,6 @@ func BuildRelayer(cfg config.Config, opts RelayerOptions) (*Services, error) { } } - if err = cfg.ValidateClients(opts.LightClients); err != nil { - return nil, errors.Wrap(err, "validation failed") - } - // Proof generators proofGenerators, err := proofgen.NewSetFromConfig( ctx, cfg, clientSet, append(local, remote...), opts.LightClients, diff --git a/link/internal/config/clients.go b/link/internal/config/clients.go deleted file mode 100644 index 6107a5aec..000000000 --- a/link/internal/config/clients.go +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "strings" - - "github.com/pkg/errors" - - "github.com/cosmos/ibc/link/lightclient" -) - -// ValidateClients checks built-in attestation parameters and validates custom -// client ends through the caller-supplied registry. -// -// Every entry point that builds a relayer must call this. -func (c Config) ValidateClients(reg *lightclient.Registry) error { - if _, shadowsBuiltin := reg.Get(string(ClientTypeAttestation)); shadowsBuiltin { - return errors.Errorf("client type %q is built in and cannot be overridden", ClientTypeAttestation) - } - - for _, conn := range c.Relayer.Connections { - for name, end := range map[string]ClientEnd{"clientA": conn.ClientA, "clientB": conn.ClientB} { - if err := validateClientEnd(reg, end); err != nil { - return errors.Wrapf(err, ".relayer.connections[%s].%s", conn.Alias, name) - } - } - } - - return nil -} - -func validateClientEnd(reg *lightclient.Registry, end ClientEnd) error { - if end.Type == ClientTypeAttestation { - if !end.ClientParams.IsEmpty() { - return errors.New(".clientParams attestation clients take no params; configure attestors under .attestors") - } - - return nil - } - - factory, ok := reg.Get(string(end.Type)) - if !ok { - registered := append([]string{string(ClientTypeAttestation)}, reg.Types()...) - return errors.Errorf( - ".type unknown client type %q (registered: [%s])", - end.Type, - strings.Join(registered, ", "), - ) - } - - if err := factory.ValidateParams(end.ClientParams); err != nil { - return errors.Wrap(err, ".clientParams") - } - - return nil -} diff --git a/link/internal/config/clients_test.go b/link/internal/config/clients_test.go deleted file mode 100644 index a8581d3e8..000000000 --- a/link/internal/config/clients_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "context" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cosmos/ibc/link/lightclient" -) - -type stubParams struct { - ProverURL string `yaml:"proverUrl"` -} - -// stubFactory stands in for a custom light client's factory. -type stubFactory struct { - requireParams bool -} - -func (f stubFactory) ValidateParams(params *lightclient.RawParams) error { - if !f.requireParams { - return nil - } - - var p stubParams - if err := params.Decode(&p); err != nil { - return err - } - - if p.ProverURL == "" { - return errRequired - } - - return nil -} - -func (f stubFactory) New( - context.Context, lightclient.FactoryOptions, -) (lightclient.ProofGenerator, error) { - return nil, nil -} - -var errRequired = errorString("proverUrl required") - -type errorString string - -func (e errorString) Error() string { return string(e) } - -func loadSample(t *testing.T) Config { - t.Helper() - - config, err := LoadFromFile(filepath.Join("testdata", "sample.yml"), true, true) - require.NoError(t, err) - - return config -} - -func TestValidateClients(t *testing.T) { - t.Run("registered type passes", func(t *testing.T) { - config := loadSample(t) - - require.NoError(t, config.ValidateClients(nil)) - }) - - t.Run("unregistered type fails and lists what is registered", func(t *testing.T) { - config := loadSample(t) - config.Relayer.Connections[0].ClientA.Type = "tendermint" - - err := config.ValidateClients(nil) - require.ErrorContains(t, err, `unknown client type "tendermint"`) - }) - - t.Run("arbitrary type name resolves once registered", func(t *testing.T) { - config := loadSample(t) - for i := range config.Relayer.Connections { - config.Relayer.Connections[i].ClientA.Type = "myclient" - config.Relayer.Connections[i].ClientB.Type = "myclient" - } - - reg := lightclient.NewRegistry() - require.NoError(t, reg.Register("myclient", stubFactory{})) - - require.NoError(t, config.ValidateClients(reg)) - }) - - t.Run("factory params error is surfaced", func(t *testing.T) { - config := loadSample(t) - for i := range config.Relayer.Connections { - config.Relayer.Connections[i].ClientA.Type = "myclient" - config.Relayer.Connections[i].ClientB.Type = "myclient" - } - - reg := lightclient.NewRegistry() - require.NoError(t, reg.Register("myclient", stubFactory{requireParams: true})) - - err := config.ValidateClients(reg) - require.ErrorContains(t, err, ".clientParams") - require.ErrorContains(t, err, "proverUrl required") - }) - - t.Run("nil registry supports built-in attestation", func(t *testing.T) { - config := loadSample(t) - - require.NoError(t, config.ValidateClients(nil)) - }) - - t.Run("attestation name cannot be overridden", func(t *testing.T) { - config := loadSample(t) - reg := lightclient.NewRegistry() - require.NoError(t, reg.Register(string(ClientTypeAttestation), stubFactory{})) - - require.ErrorContains(t, config.ValidateClients(reg), "cannot be overridden") - }) - - t.Run("attestation params are rejected without a factory", func(t *testing.T) { - config := loadSample(t) - config.Relayer.Connections[0].ClientA.ClientParams = lightclient.NewRawParams([]byte("extra: true\n")) - - require.ErrorContains(t, config.ValidateClients(nil), "attestation clients take no params") - }) -} - -// 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 := lightclient.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 := lightclient.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 *lightclient.RawParams - - var p stubParams - require.NoError(t, params.Decode(&p)) - require.True(t, params.IsEmpty()) -} diff --git a/link/internal/config/relayer_test.go b/link/internal/config/relayer_test.go index 80d8a2fff..883af35f0 100644 --- a/link/internal/config/relayer_test.go +++ b/link/internal/config/relayer_test.go @@ -198,9 +198,8 @@ func TestRelayerConfig(t *testing.T) { errContains: ".chainId required", }, { - // An unregistered type is no longer a structural error: client - // types are open, and validating one needs a registry. See - // TestValidateClients for the check that replaced this. + // An unregistered type is not a structural config error. It is + // resolved when the relayer constructs proof generators. name: "unregistered client type passes structural validation", patch: func(c *Config) { c.Relayer.Connections[0].ClientA.Type = "tendermint" diff --git a/link/internal/livevalidate/quorum.go b/link/internal/livevalidate/quorum.go index 4d5e44734..cc37c5eae 100644 --- a/link/internal/livevalidate/quorum.go +++ b/link/internal/livevalidate/quorum.go @@ -33,10 +33,6 @@ func checkAttestorQuorum(ctx context.Context, cfg config.Config, clientSet *chai attestors = append(attestors, local...) attestors = append(attestors, remote...) - if err := cfg.ValidateClients(nil); err != nil { - return errors.Wrap(err, "client config") - } - 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 053642a2a..5ff771667 100644 --- a/link/internal/livevalidate/quorum_test.go +++ b/link/internal/livevalidate/quorum_test.go @@ -139,7 +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, `unknown client type "tendermint"`) + require.ErrorContains(t, err, `no proof generator registered for client type "tendermint"`) require.ErrorContains(t, err, "attestation", "the error should name what is registered") }) } diff --git a/link/internal/relay/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index a5f615141..62eb29d73 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -57,9 +57,13 @@ func NewSetFromConfig( reg *lightclient.Registry, ) (*Set, error) { generators := make(map[string]ProofGenerator, 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, reg) + return addGenerator(ctx, generators, connAlias, self, counterparty, chainInfos, clientSet, attestors, reg) }) if err != nil { return nil, err @@ -92,6 +96,7 @@ func addGenerator( generators map[string]ProofGenerator, connAlias string, client, clientCounterparty config.ClientEnd, + chainInfo map[string]lightclient.ChainInfo, clientSet *chains.ClientSet, attestors []attestorservice.Attestor, reg *lightclient.Registry, @@ -111,16 +116,29 @@ func addGenerator( factory, ok := reg.Get(string(client.Type)) if !ok { + registered := append([]string{string(config.ClientTypeAttestation)}, reg.Types()...) return errors.Errorf( "connection %q: no proof generator registered for client type %q (registered: %v)", - connAlias, client.Type, reg.Types(), + 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.FactoryOptions{ - Client: toClientInfo(client, clientCounterparty.ChainID), + Client: toClientInfo(client, clientCounterparty.ChainID), + HostChain: hostChain, + CounterpartyChain: counterpartyChain, }, ) if err != nil { @@ -132,6 +150,18 @@ func addGenerator( 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, counterpartyChainID string) lightclient.ClientInfo { return lightclient.ClientInfo{ ChainID: end.ChainID, diff --git a/link/internal/relay/proofgen/proofgen_test.go b/link/internal/relay/proofgen/proofgen_test.go index 68bbb1a07..550f74eae 100644 --- a/link/internal/relay/proofgen/proofgen_test.go +++ b/link/internal/relay/proofgen/proofgen_test.go @@ -107,7 +107,13 @@ 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, nil) @@ -129,13 +135,25 @@ 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}}, + } reg := lightclient.NewRegistry() - require.NoError(t, reg.Register("myclient", stubFactory{})) + built := make(chan lightclient.FactoryOptions, 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) @@ -143,13 +161,18 @@ func TestNewSetFromConfig(t *testing.T) { } // stubFactory is a light client type that exists only in this test. -type stubFactory struct{} +type stubFactory struct { + built chan<- lightclient.FactoryOptions +} -func (stubFactory) ValidateParams(*lightclient.RawParams) error { return nil } +func (stubFactory) Type() string { return "myclient" } -func (stubFactory) New( - context.Context, lightclient.FactoryOptions, +func (f stubFactory) New( + _ context.Context, options lightclient.FactoryOptions, ) (lightclient.ProofGenerator, error) { + if f.built != nil { + f.built <- options + } return stubGenerator{}, nil } diff --git a/link/lightclient/params.go b/link/lightclient/params.go index aa549b043..bd26c7d5e 100644 --- a/link/lightclient/params.go +++ b/link/lightclient/params.go @@ -8,7 +8,7 @@ import ( ) // RawParams is a client-type-specific config block captured verbatim at decode -// time and interpreted by the Factory registered for that type, not by the +// time and interpreted by the ProverFactory registered for that type, not by the // config package. // // It is used as a pointer in config structs so that adding it does not make @@ -39,7 +39,7 @@ func (p RawParams) MarshalYAML() ([]byte, error) { return p.raw, nil } -// IsEmpty reports whether no params were configured. A Factory that requires +// IsEmpty reports whether no params were configured. A ProverFactory that requires // params should check this and return a helpful error rather than decoding an // empty document into a zero value. func (p *RawParams) IsEmpty() bool { 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 index aa727c238..d8c963a30 100644 --- a/link/lightclient/proofgen.go +++ b/link/lightclient/proofgen.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Package lightclient is the public extension point for light client proof -// generation. Implement ProofGenerator and Factory here, register the factory +// generation. Implement ProofGenerator and ProverFactory here, register the factory // under a client type name, and supply it through link/cli. // // This package must not import anything under link/internal. Everything it @@ -52,7 +52,7 @@ const ( ) // ClientInfo describes one configured light-client instance from the -// perspective of the Factory responsible for its type. +// perspective of the ProverFactory responsible for its type. type ClientInfo struct { ChainID string CounterpartyChainID string @@ -61,9 +61,24 @@ type ClientInfo struct { ClientParams *RawParams } -// FactoryOptions describes the configured light-client instance a Factory +// ChainInfo is the chain configuration relevant to proof generation. It omits +// operational settings, such as the deployer, that custom provers do not need. +type ChainInfo struct { + ChainID string + EVM *EVMChainInfo +} + +// EVMChainInfo contains the EVM connection details available to a prover. +type EVMChainInfo struct { + RPC string + ICS26Router string +} + +// FactoryOptions describes the configured light-client instance a ProverFactory // constructs. Additional shared construction dependencies may be added here // as the extension API evolves. type FactoryOptions struct { - Client ClientInfo + Client ClientInfo + HostChain ChainInfo + CounterpartyChain ChainInfo } diff --git a/link/lightclient/registry.go b/link/lightclient/registry.go index df4a836f6..9c518176d 100644 --- a/link/lightclient/registry.go +++ b/link/lightclient/registry.go @@ -9,17 +9,13 @@ import ( "github.com/pkg/errors" ) -// Factory builds proof generators for one light client type and validates that -// type's config params. -type Factory interface { - // ValidateParams reports whether params are usable for this client type. - // It runs during config validation, before any chain access, so it must - // not perform I/O. Decode through RawParams.Decode so unknown fields are - // rejected. - ValidateParams(params *RawParams) error - - // New builds a generator for Client, which tracks Counterparty. ClientParams - // have already passed ValidateParams. +// ProverFactory builds proof generators for one custom light-client type. +// New validates the client parameters as part of construction. +type ProverFactory interface { + // Type returns the name operators use in connection configuration. + Type() string + + // New builds a generator for Client, which tracks CounterpartyChain. New(ctx context.Context, options FactoryOptions) (ProofGenerator, error) } @@ -29,22 +25,25 @@ type Factory interface { // A Registry is not safe for concurrent registration. Build it fully during // startup, then treat it as read-only. type Registry struct { - factories map[string]Factory + factories map[string]ProverFactory } func NewRegistry() *Registry { - return &Registry{factories: make(map[string]Factory)} + return &Registry{factories: make(map[string]ProverFactory)} } -// Register associates a client type name with a factory. The name is the -// string operators write as `type:` in their connection config. Registering a -// name twice is an error rather than a silent overwrite. -func (r *Registry) Register(clientType string, f Factory) error { - switch { - case clientType == "": +// Register associates a factory with the client type returned by Type. +// Registering a type twice is an error rather than a silent overwrite. +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 f == nil: - return errors.Errorf("client type %q: factory must not be nil", clientType) + case "attestation": + return errors.Errorf("client type %q is built in and cannot be overridden", clientType) } if _, exists := r.factories[clientType]; exists { @@ -56,7 +55,7 @@ func (r *Registry) Register(clientType string, f Factory) error { return nil } -func (r *Registry) Get(clientType string) (Factory, bool) { +func (r *Registry) Get(clientType string) (ProverFactory, bool) { if r == nil { return nil, false } diff --git a/link/lightclient/registry_test.go b/link/lightclient/registry_test.go new file mode 100644 index 000000000..5db616f7a --- /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, FactoryOptions) (ProofGenerator, 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/remote.go b/link/lightclient/remotepoc/remote.go index cd0b7abfc..6594a7db0 100644 --- a/link/lightclient/remotepoc/remote.go +++ b/link/lightclient/remotepoc/remote.go @@ -31,19 +31,7 @@ type Factory struct { HTTPClient *http.Client } -func (Factory) ValidateParams(raw *lightclient.RawParams) error { - var p ClientParams - if err := raw.Decode(&p); err != nil { - return err - } - - u, err := url.ParseRequestURI(p.URL) - if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { - return errors.New("url must be an absolute HTTP(S) URL") - } - - return nil -} +func (Factory) Type() string { return Type } func (f Factory) New( _ context.Context, @@ -53,6 +41,10 @@ func (f Factory) New( 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 { From 2b51b5c703d4a5d5934ec0a8ca6c84df9dda3ae0 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 17:23:20 -0700 Subject: [PATCH 06/17] refactor(link): rename proof generator API to prover Signed-off-by: Dennis Fang --- .../customlightclient/customlightclient.go | 8 ++- link/.mockery.yaml | 2 +- link/internal/relay/dispatch/fixtures_test.go | 8 +-- link/internal/relay/pipeline/pipeline_test.go | 14 ++-- .../relay/processors/batch_ack_packet.go | 2 +- .../relay/processors/batch_recv_packet.go | 2 +- .../processors/batch_recv_packet_test.go | 10 +-- link/internal/relay/processors/batch_relay.go | 2 +- .../relay/processors/check_send_finality.go | 2 +- .../processors/check_send_finality_test.go | 6 +- .../processors/check_timeout_finality.go | 2 +- .../processors/check_timeout_finality_test.go | 6 +- .../processors/check_write_ack_finality.go | 2 +- .../check_write_ack_finality_test.go | 6 +- link/internal/relay/processors/processors.go | 2 +- .../relay/processors/timeout_packet.go | 2 +- .../relay/proofgen/attestation/generator.go | 2 +- link/internal/relay/proofgen/proofgen.go | 36 +++++----- link/internal/relay/proofgen/proofgen_test.go | 10 +-- link/internal/relay/txbuilder/evm/evm.go | 2 +- link/internal/tests/mocks/proofgen.go | 72 +++++++++---------- link/lightclient/proofgen.go | 18 +++-- link/lightclient/registry.go | 2 +- link/lightclient/registry_test.go | 2 +- .../remotepoc/attestation_handler.go | 2 +- link/lightclient/remotepoc/remote.go | 4 +- 26 files changed, 112 insertions(+), 114 deletions(-) diff --git a/e2e/internal/customlightclient/customlightclient.go b/e2e/internal/customlightclient/customlightclient.go index 2ad94a25b..9d6e91089 100644 --- a/e2e/internal/customlightclient/customlightclient.go +++ b/e2e/internal/customlightclient/customlightclient.go @@ -27,8 +27,8 @@ func (Factory) Type() string { return Type } func (Factory) New( _ context.Context, - options lightclient.FactoryOptions, -) (lightclient.ProofGenerator, error) { + options lightclient.ProverFactoryOptions, +) (lightclient.Prover, error) { self := options.Client var p params if err := self.ClientParams.Decode(&p); err != nil { @@ -37,7 +37,9 @@ func (Factory) New( if p.MarkerFile == "" { return nil, errors.New("markerFile is required") } - if err := os.WriteFile(p.MarkerFile, []byte(self.ChainID+"/"+self.ClientID), 0o600); err != nil { + if err := os.WriteFile( + p.MarkerFile, []byte(options.HostChain.ChainID+"/"+self.ClientID), 0o600, + ); err != nil { return nil, err } 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/internal/relay/dispatch/fixtures_test.go b/link/internal/relay/dispatch/fixtures_test.go index 4df3ba66f..4b1be2a66 100644 --- a/link/internal/relay/dispatch/fixtures_test.go +++ b/link/internal/relay/dispatch/fixtures_test.go @@ -42,9 +42,9 @@ func (s staticChains) Get(chainID string) (chains.Client, bool) { return client, ok } -type staticProofGenerators map[string]proofgen.ProofGenerator +type staticProofGenerators map[string]proofgen.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.ProofGenerator, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } @@ -70,8 +70,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..13b358e46 100644 --- a/link/internal/relay/pipeline/pipeline_test.go +++ b/link/internal/relay/pipeline/pipeline_test.go @@ -44,8 +44,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 +59,9 @@ func (s staticChains) Get(chainID string) (chains.Client, bool) { return client, ok } -type staticProofGenerators map[string]proofgen.ProofGenerator +type staticProofGenerators map[string]proofgen.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.ProofGenerator, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } @@ -87,8 +87,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 +121,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..eb046e228 100644 --- a/link/internal/relay/processors/batch_ack_packet.go +++ b/link/internal/relay/processors/batch_ack_packet.go @@ -24,7 +24,7 @@ type BatchAckPacket struct { destinationChainClient chains.Client sourceChainClient chains.Client route Route - proofGen proofgen.ProofGenerator + proofGen proofgen.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..3f4a7c3e2 100644 --- a/link/internal/relay/processors/batch_recv_packet.go +++ b/link/internal/relay/processors/batch_recv_packet.go @@ -24,7 +24,7 @@ type BatchRecvPacket struct { sourceChainClient chains.Client destinationChainClient chains.Client route Route - proofGen proofgen.ProofGenerator + proofGen proofgen.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 086937cb1..1c64e7860 100644 --- a/link/internal/relay/processors/batch_recv_packet_test.go +++ b/link/internal/relay/processors/batch_recv_packet_test.go @@ -31,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]proofgen.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.ProofGenerator, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } @@ -102,7 +102,7 @@ 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), lightclient.ProofKindPacketCommitment, mock.Anything). @@ -199,7 +199,7 @@ 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), lightclient.ProofKindPacketCommitment, mock.Anything). @@ -304,7 +304,7 @@ 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), lightclient.ProofKindPacketCommitment, mock.Anything). diff --git a/link/internal/relay/processors/batch_relay.go b/link/internal/relay/processors/batch_relay.go index 39c4672ff..db2de4158 100644 --- a/link/internal/relay/processors/batch_relay.go +++ b/link/internal/relay/processors/batch_relay.go @@ -72,7 +72,7 @@ func proofKindFor(relayKind v2.RelayKind) lightclient.ProofKind { func relayPackets( ctx context.Context, chainClient chains.Client, - proofGen proofgen.ProofGenerator, + proofGen proofgen.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..40b851ad7 100644 --- a/link/internal/relay/processors/check_send_finality.go +++ b/link/internal/relay/processors/check_send_finality.go @@ -20,7 +20,7 @@ import ( // prove. type CheckSendFinality struct { sourceChainClient chains.Client - proofGen proofgen.ProofGenerator + proofGen proofgen.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..3906dcf7b 100644 --- a/link/internal/relay/processors/check_timeout_finality.go +++ b/link/internal/relay/processors/check_timeout_finality.go @@ -16,7 +16,7 @@ import ( // proof generator currently being able to prove a destination-chain // timestamp past the timeout. type CheckTimeoutFinality struct { - proofGen proofgen.ProofGenerator + proofGen proofgen.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..934ca5df8 100644 --- a/link/internal/relay/processors/check_write_ack_finality.go +++ b/link/internal/relay/processors/check_write_ack_finality.go @@ -20,7 +20,7 @@ import ( // currently prove. type CheckWriteAckFinality struct { destinationChainClient chains.Client - proofGen proofgen.ProofGenerator + proofGen proofgen.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..1ca243eef 100644 --- a/link/internal/relay/processors/processors.go +++ b/link/internal/relay/processors/processors.go @@ -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) (proofgen.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..c2e8f2db2 100644 --- a/link/internal/relay/processors/timeout_packet.go +++ b/link/internal/relay/processors/timeout_packet.go @@ -22,7 +22,7 @@ import ( type BatchTimeoutPacket struct { sourceChainClient chains.Client route Route - proofGen proofgen.ProofGenerator + proofGen proofgen.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 11fccc4da..5fd50b58b 100644 --- a/link/internal/relay/proofgen/attestation/generator.go +++ b/link/internal/relay/proofgen/attestation/generator.go @@ -16,7 +16,7 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// Generator implements lightclient.ProofGenerator for one configured +// Generator implements lightclient.Prover for one configured // attestation light client: LatestProvableHeight/StateProof/PacketProofs all // query the same fixed attestor set with the same quorum threshold type Generator struct { diff --git a/link/internal/relay/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index 62eb29d73..4279344d1 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -17,36 +17,36 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// ProofGenerator is the public light-client proof generation contract. +// Prover is the public light-client proof generation contract. // Keep the alias here so existing internal relay code can continue to refer to // the package that owns generator resolution and lookup. -type ProofGenerator = lightclient.ProofGenerator +type Prover = lightclient.Prover // Key identifies one configured light client by the chain it lives on and -// its client id, the composite key ProofGenerator instances are scoped by. +// its client id, the composite key Prover instances are scoped by. func Key(chainID, clientID string) string { return chainID + "/" + clientID } -// Set resolves a ProofGenerator by (chainID, clientID). +// Set resolves a Prover by (chainID, clientID). type Set struct { - generators map[string]ProofGenerator + generators map[string]Prover } -func NewSet(generators map[string]ProofGenerator) *Set { +func NewSet(generators map[string]Prover) *Set { if generators == nil { - generators = make(map[string]ProofGenerator) + generators = make(map[string]Prover) } return &Set{generators: generators} } -func (s *Set) Get(chainID, clientID string) (ProofGenerator, bool) { +func (s *Set) Get(chainID, clientID string) (Prover, bool) { generator, ok := s.generators[Key(chainID, clientID)] return generator, ok } -// NewSetFromConfig resolves a ProofGenerator for every client end of every +// NewSetFromConfig resolves a Prover for every client end of every // configured connection. Attestation uses its built-in resolver; other client // types dispatch through the caller-supplied registry. func NewSetFromConfig( @@ -56,7 +56,7 @@ func NewSetFromConfig( attestors []attestorservice.Attestor, reg *lightclient.Registry, ) (*Set, error) { - generators := make(map[string]ProofGenerator, len(cfg.Relayer.Connections)*2) + generators := make(map[string]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) @@ -93,7 +93,7 @@ func forEachClientEnd(cfg config.Config, fn func(connAlias string, self, counter func addGenerator( ctx context.Context, - generators map[string]ProofGenerator, + generators map[string]Prover, connAlias string, client, clientCounterparty config.ClientEnd, chainInfo map[string]lightclient.ChainInfo, @@ -135,8 +135,8 @@ func addGenerator( gen, err := factory.New( ctx, - lightclient.FactoryOptions{ - Client: toClientInfo(client, clientCounterparty.ChainID), + lightclient.ProverFactoryOptions{ + Client: toClientInfo(client), HostChain: hostChain, CounterpartyChain: counterpartyChain, }, @@ -162,12 +162,10 @@ func toChainInfo(chain config.ChainConfig) lightclient.ChainInfo { return info } -func toClientInfo(end config.ClientEnd, counterpartyChainID string) lightclient.ClientInfo { +func toClientInfo(end config.ClientEnd) lightclient.ClientInfo { return lightclient.ClientInfo{ - ChainID: end.ChainID, - CounterpartyChainID: counterpartyChainID, - ClientID: end.ClientID, - Type: string(end.Type), - ClientParams: end.ClientParams, + 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 550f74eae..d9b11b17d 100644 --- a/link/internal/relay/proofgen/proofgen_test.go +++ b/link/internal/relay/proofgen/proofgen_test.go @@ -144,7 +144,7 @@ func TestNewSetFromConfig(t *testing.T) { } reg := lightclient.NewRegistry() - built := make(chan lightclient.FactoryOptions, 2) + built := make(chan lightclient.ProverFactoryOptions, 2) require.NoError(t, reg.Register(stubFactory{built: built})) set, err := NewSetFromConfig(ctx, cfg, clientSet, nil, reg) @@ -162,18 +162,18 @@ func TestNewSetFromConfig(t *testing.T) { // stubFactory is a light client type that exists only in this test. type stubFactory struct { - built chan<- lightclient.FactoryOptions + built chan<- lightclient.ProverFactoryOptions } func (stubFactory) Type() string { return "myclient" } func (f stubFactory) New( - _ context.Context, options lightclient.FactoryOptions, -) (lightclient.ProofGenerator, error) { + _ context.Context, options lightclient.ProverFactoryOptions, +) (lightclient.Prover, error) { if f.built != nil { f.built <- options } return stubGenerator{}, nil } -type stubGenerator struct{ lightclient.ProofGenerator } +type stubGenerator struct{ lightclient.Prover } diff --git a/link/internal/relay/txbuilder/evm/evm.go b/link/internal/relay/txbuilder/evm/evm.go index 81a5646ed..00202fa7e 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 lightclient.ProofGenerator.StateProof. +// updateMsg is the already-encoded proof produced by lightclient.Prover.StateProof. 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 94cfcc2bd..3ab1ba109 100644 --- a/link/internal/tests/mocks/proofgen.go +++ b/link/internal/tests/mocks/proofgen.go @@ -12,13 +12,13 @@ import ( "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 lightclient.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 { @@ -133,8 +133,8 @@ 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 } @@ -143,11 +143,11 @@ type MockProofGenerator_PacketProofs_Call struct { // - height uint64 // - 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 lightclient.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 { @@ -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 lightclient.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/lightclient/proofgen.go b/link/lightclient/proofgen.go index d8c963a30..b96de705e 100644 --- a/link/lightclient/proofgen.go +++ b/link/lightclient/proofgen.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Package lightclient is the public extension point for light client proof -// generation. Implement ProofGenerator and ProverFactory here, register the factory +// generation. Implement Prover and ProverFactory here, register the factory // under a client type name, and supply it through link/cli. // // This package must not import anything under link/internal. Everything it @@ -16,9 +16,9 @@ import ( channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" ) -// ProofGenerator generates packet membership/non-membership proofs and state +// Prover generates packet membership/non-membership proofs and state // proofs for one configured light client. -type ProofGenerator interface { +type Prover 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. @@ -54,11 +54,9 @@ const ( // ClientInfo describes one configured light-client instance from the // perspective of the ProverFactory responsible for its type. type ClientInfo struct { - ChainID string - CounterpartyChainID string - ClientID string - Type string - ClientParams *RawParams + ClientID string + Type string + ClientParams *RawParams } // ChainInfo is the chain configuration relevant to proof generation. It omits @@ -74,10 +72,10 @@ type EVMChainInfo struct { ICS26Router string } -// FactoryOptions describes the configured light-client instance a ProverFactory +// ProverFactoryOptions describes the configured light-client instance a ProverFactory // constructs. Additional shared construction dependencies may be added here // as the extension API evolves. -type FactoryOptions struct { +type ProverFactoryOptions struct { Client ClientInfo HostChain ChainInfo CounterpartyChain ChainInfo diff --git a/link/lightclient/registry.go b/link/lightclient/registry.go index 9c518176d..67a44627d 100644 --- a/link/lightclient/registry.go +++ b/link/lightclient/registry.go @@ -16,7 +16,7 @@ type ProverFactory interface { Type() string // New builds a generator for Client, which tracks CounterpartyChain. - New(ctx context.Context, options FactoryOptions) (ProofGenerator, error) + New(ctx context.Context, options ProverFactoryOptions) (Prover, error) } // Registry resolves custom light-client factories by config type name. diff --git a/link/lightclient/registry_test.go b/link/lightclient/registry_test.go index 5db616f7a..90a336d78 100644 --- a/link/lightclient/registry_test.go +++ b/link/lightclient/registry_test.go @@ -13,7 +13,7 @@ type testFactory string func (f testFactory) Type() string { return string(f) } -func (testFactory) New(context.Context, FactoryOptions) (ProofGenerator, error) { +func (testFactory) New(context.Context, ProverFactoryOptions) (Prover, error) { return nil, nil } diff --git a/link/lightclient/remotepoc/attestation_handler.go b/link/lightclient/remotepoc/attestation_handler.go index c49f55d74..0250bd552 100644 --- a/link/lightclient/remotepoc/attestation_handler.go +++ b/link/lightclient/remotepoc/attestation_handler.go @@ -20,7 +20,7 @@ import ( ) // NewHandler exposes generator through the proof-of-concept HTTP protocol. -func NewHandler(generator lightclient.ProofGenerator) *http.Server { +func NewHandler(generator lightclient.Prover) *http.Server { mux := http.NewServeMux() mux.HandleFunc("POST /proof", func(w http.ResponseWriter, r *http.Request) { var req request diff --git a/link/lightclient/remotepoc/remote.go b/link/lightclient/remotepoc/remote.go index 6594a7db0..1305f6b2b 100644 --- a/link/lightclient/remotepoc/remote.go +++ b/link/lightclient/remotepoc/remote.go @@ -35,8 +35,8 @@ func (Factory) Type() string { return Type } func (f Factory) New( _ context.Context, - options lightclient.FactoryOptions, -) (lightclient.ProofGenerator, error) { + options lightclient.ProverFactoryOptions, +) (lightclient.Prover, error) { var p ClientParams if err := options.Client.ClientParams.Decode(&p); err != nil { return nil, err From 55da9de59d78d0449be71be407c7de74975edeb9 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 17:26:15 -0700 Subject: [PATCH 07/17] refactor(link): colocate prover factory types Signed-off-by: Dennis Fang --- link/lightclient/proofgen.go | 30 ------------------------------ link/lightclient/registry.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/link/lightclient/proofgen.go b/link/lightclient/proofgen.go index b96de705e..d96dcb015 100644 --- a/link/lightclient/proofgen.go +++ b/link/lightclient/proofgen.go @@ -50,33 +50,3 @@ const ( ProofKindAcknowledgement ProofKindReceiptAbsence ) - -// ClientInfo describes one configured light-client instance from the -// perspective of the ProverFactory responsible for its type. -type ClientInfo struct { - ClientID string - Type string - ClientParams *RawParams -} - -// ChainInfo is the chain configuration relevant to proof generation. It omits -// operational settings, such as the deployer, that custom provers do not need. -type ChainInfo struct { - ChainID string - EVM *EVMChainInfo -} - -// EVMChainInfo contains the EVM connection details available to a prover. -type EVMChainInfo struct { - RPC string - ICS26Router string -} - -// ProverFactoryOptions describes the configured light-client instance a ProverFactory -// constructs. Additional shared construction dependencies may be added here -// as the extension API evolves. -type ProverFactoryOptions struct { - Client ClientInfo - HostChain ChainInfo - CounterpartyChain ChainInfo -} diff --git a/link/lightclient/registry.go b/link/lightclient/registry.go index 67a44627d..bf6e282b7 100644 --- a/link/lightclient/registry.go +++ b/link/lightclient/registry.go @@ -9,6 +9,36 @@ import ( "github.com/pkg/errors" ) +// ClientInfo describes one configured light-client instance from the +// perspective of the ProverFactory responsible for its type. +type ClientInfo struct { + ClientID string + Type string + ClientParams *RawParams +} + +// ChainInfo is the chain configuration relevant to proof generation. It omits +// operational settings, such as the deployer, that custom provers do not need. +type ChainInfo struct { + ChainID string + EVM *EVMChainInfo +} + +// EVMChainInfo contains the EVM connection details available to a prover. +type EVMChainInfo struct { + RPC string + ICS26Router string +} + +// ProverFactoryOptions describes the configured light-client instance a +// ProverFactory constructs. Additional shared construction dependencies may +// be added here as the extension API evolves. +type ProverFactoryOptions struct { + Client ClientInfo + HostChain ChainInfo + CounterpartyChain ChainInfo +} + // ProverFactory builds proof generators for one custom light-client type. // New validates the client parameters as part of construction. type ProverFactory interface { From e3a8db8d3f2b6a54a1687bbeed9cea63799cccb6 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 17:29:01 -0700 Subject: [PATCH 08/17] refactor(link): remove internal prover alias Signed-off-by: Dennis Fang --- link/internal/relay/dispatch/fixtures_test.go | 5 +++-- link/internal/relay/pipeline/pipeline_test.go | 5 +++-- .../relay/processors/batch_ack_packet.go | 4 ++-- .../relay/processors/batch_recv_packet.go | 4 ++-- .../relay/processors/batch_recv_packet_test.go | 4 ++-- link/internal/relay/processors/batch_relay.go | 3 +-- .../relay/processors/check_send_finality.go | 4 ++-- .../relay/processors/check_timeout_finality.go | 4 ++-- .../processors/check_write_ack_finality.go | 4 ++-- link/internal/relay/processors/processors.go | 4 ++-- .../internal/relay/processors/timeout_packet.go | 4 ++-- link/internal/relay/proofgen/proofgen.go | 17 ++++++----------- 12 files changed, 29 insertions(+), 33 deletions(-) diff --git a/link/internal/relay/dispatch/fixtures_test.go b/link/internal/relay/dispatch/fixtures_test.go index 4b1be2a66..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.Prover +type staticProofGenerators map[string]lightclient.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.Prover, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (lightclient.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } diff --git a/link/internal/relay/pipeline/pipeline_test.go b/link/internal/relay/pipeline/pipeline_test.go index 13b358e46..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 ( @@ -59,9 +60,9 @@ func (s staticChains) Get(chainID string) (chains.Client, bool) { return client, ok } -type staticProofGenerators map[string]proofgen.Prover +type staticProofGenerators map[string]lightclient.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.Prover, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (lightclient.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } diff --git a/link/internal/relay/processors/batch_ack_packet.go b/link/internal/relay/processors/batch_ack_packet.go index eb046e228..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.Prover + 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 3f4a7c3e2..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.Prover + 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 1c64e7860..6ed4422ba 100644 --- a/link/internal/relay/processors/batch_recv_packet_test.go +++ b/link/internal/relay/processors/batch_recv_packet_test.go @@ -31,9 +31,9 @@ func (s staticChains) Get(chainID string) (chains.Client, bool) { return client, ok } -type staticProofGenerators map[string]proofgen.Prover +type staticProofGenerators map[string]lightclient.Prover -func (s staticProofGenerators) Get(chainID, clientID string) (proofgen.Prover, bool) { +func (s staticProofGenerators) Get(chainID, clientID string) (lightclient.Prover, bool) { gen, ok := s[proofgen.Key(chainID, clientID)] return gen, ok } diff --git a/link/internal/relay/processors/batch_relay.go b/link/internal/relay/processors/batch_relay.go index db2de4158..323a5cc7f 100644 --- a/link/internal/relay/processors/batch_relay.go +++ b/link/internal/relay/processors/batch_relay.go @@ -10,7 +10,6 @@ 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" @@ -72,7 +71,7 @@ func proofKindFor(relayKind v2.RelayKind) lightclient.ProofKind { func relayPackets( ctx context.Context, chainClient chains.Client, - proofGen proofgen.Prover, + 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 40b851ad7..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.Prover + proofGen lightclient.Prover } func NewCheckSendFinality( diff --git a/link/internal/relay/processors/check_timeout_finality.go b/link/internal/relay/processors/check_timeout_finality.go index 3906dcf7b..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.Prover + proofGen lightclient.Prover } func NewCheckTimeoutFinality(proofGenerators ProofGenerators, route Route) (CheckTimeoutFinality, error) { diff --git a/link/internal/relay/processors/check_write_ack_finality.go b/link/internal/relay/processors/check_write_ack_finality.go index 934ca5df8..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.Prover + proofGen lightclient.Prover } func NewCheckWriteAckFinality( diff --git a/link/internal/relay/processors/processors.go b/link/internal/relay/processors/processors.go index 1ca243eef..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.Prover, 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 c2e8f2db2..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.Prover + proofGen lightclient.Prover txBuilder txbuilder.TxBuilder txSubmitter txsubmitter.TxSubmitter storage TxStorage diff --git a/link/internal/relay/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index 4279344d1..27fd66114 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -17,11 +17,6 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// Prover is the public light-client proof generation contract. -// Keep the alias here so existing internal relay code can continue to refer to -// the package that owns generator resolution and lookup. -type Prover = lightclient.Prover - // Key identifies one configured light client by the chain it lives on and // its client id, the composite key Prover instances are scoped by. func Key(chainID, clientID string) string { @@ -30,18 +25,18 @@ func Key(chainID, clientID string) string { // Set resolves a Prover by (chainID, clientID). type Set struct { - generators map[string]Prover + generators map[string]lightclient.Prover } -func NewSet(generators map[string]Prover) *Set { +func NewSet(generators map[string]lightclient.Prover) *Set { if generators == nil { - generators = make(map[string]Prover) + generators = make(map[string]lightclient.Prover) } return &Set{generators: generators} } -func (s *Set) Get(chainID, clientID string) (Prover, bool) { +func (s *Set) Get(chainID, clientID string) (lightclient.Prover, bool) { generator, ok := s.generators[Key(chainID, clientID)] return generator, ok } @@ -56,7 +51,7 @@ func NewSetFromConfig( attestors []attestorservice.Attestor, reg *lightclient.Registry, ) (*Set, error) { - generators := make(map[string]Prover, 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) @@ -93,7 +88,7 @@ func forEachClientEnd(cfg config.Config, fn func(connAlias string, self, counter func addGenerator( ctx context.Context, - generators map[string]Prover, + generators map[string]lightclient.Prover, connAlias string, client, clientCounterparty config.ClientEnd, chainInfo map[string]lightclient.ChainInfo, From 9c886cd35de15bb22aacc3b087aa0d7474fc134f Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 19:54:47 -0700 Subject: [PATCH 09/17] docs(link): tighten custom prover comments Signed-off-by: Dennis Fang --- e2e/custom_light_client_cli_test.go | 8 ++--- .../customlightclient/customlightclient.go | 3 +- link/cli/root.go | 11 ++++--- link/internal/config/relayer.go | 4 +-- .../relay/proofgen/attestation/generator.go | 4 +-- link/internal/relay/proofgen/proofgen.go | 16 ++++------ link/internal/relay/txbuilder/evm/evm.go | 2 +- link/lightclient/params.go | 26 ++++------------ link/lightclient/proofgen.go | 26 ++++------------ link/lightclient/registry.go | 30 +++++++------------ .../remotepoc/attestation_handler.go | 5 ++-- link/lightclient/remotepoc/remote.go | 9 +++--- 12 files changed, 43 insertions(+), 101 deletions(-) diff --git a/e2e/custom_light_client_cli_test.go b/e2e/custom_light_client_cli_test.go index 2b2fad3cb..097447ed3 100644 --- a/e2e/custom_light_client_cli_test.go +++ b/e2e/custom_light_client_cli_test.go @@ -24,9 +24,7 @@ import ( "github.com/cosmos/ibc/link/lightclient/remotepoc" ) -// TestCustomCompiledCLILoadsLightClient proves that a downstream binary can -// register a light-client implementation and select it through the ordinary -// `ibc relayer run` path. +// TestCustomCompiledCLILoadsLightClient runs a registered custom client. func TestCustomCompiledCLILoadsLightClient(t *testing.T) { binary := buildCustomIBC(t) t.Setenv("IBC_BIN", binary) @@ -59,9 +57,7 @@ func TestCustomCompiledCLILoadsLightClient(t *testing.T) { require.NotEmpty(t, createdFor) } -// TestRemoteAttestationLightClientRelaysPacket proves a custom-compiled CLI can -// complete a packet relay using the built-in attestation generator through the -// remote light-client HTTP adapter. +// TestRemoteAttestationLightClientRelaysPacket relays through a remote prover. func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { binary := buildCustomIBC(t) t.Setenv("IBC_BIN", binary) diff --git a/e2e/internal/customlightclient/customlightclient.go b/e2e/internal/customlightclient/customlightclient.go index 9d6e91089..dc0bef85d 100644 --- a/e2e/internal/customlightclient/customlightclient.go +++ b/e2e/internal/customlightclient/customlightclient.go @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// Package customlightclient is an external light-client implementation used -// to validate custom-compiled IBC binaries. +// Package customlightclient provides an E2E custom prover. package customlightclient import ( diff --git a/link/cli/root.go b/link/cli/root.go index 73145cd82..f50f2923e 100644 --- a/link/cli/root.go +++ b/link/cli/root.go @@ -18,14 +18,14 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// Options supplies dependencies for command families in the IBC CLI. +// Options configures the CLI. type Options struct { Relayer RelayerOptions } -// RelayerOptions supplies extensions used by the relayer commands. +// RelayerOptions configures relayer commands. type RelayerOptions struct { - // LightClients contains client types beyond the relayer's built-ins. + // LightClients contains custom client types. LightClients *lightclient.Registry } @@ -45,15 +45,14 @@ var rootCmd = &cobra.Command{ Short: "IBC Link", } -// NewRootCmd constructs the complete IBC CLI with the supplied extensions. -// It is intended to be called once by an executable's main function. +// NewRootCmd constructs the IBC CLI. func NewRootCmd(opts Options) *cobra.Command { relayerOptions = opts.Relayer return rootCmd } -// Execute runs root with signal cancellation and returns a process exit code. +// 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() diff --git a/link/internal/config/relayer.go b/link/internal/config/relayer.go index 84924602d..895d149ff 100644 --- a/link/internal/config/relayer.go +++ b/link/internal/config/relayer.go @@ -66,9 +66,7 @@ type ClientEnd struct { ClientID string `yaml:"clientId"` Type ClientType `yaml:"type"` - // Params is the client-type-specific config block, captured verbatim and - // interpreted by the proof generator factory registered for Type. A - // pointer so that adding it keeps ClientEnd comparable. + // 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 diff --git a/link/internal/relay/proofgen/attestation/generator.go b/link/internal/relay/proofgen/attestation/generator.go index 5fd50b58b..b52bc5c32 100644 --- a/link/internal/relay/proofgen/attestation/generator.go +++ b/link/internal/relay/proofgen/attestation/generator.go @@ -16,9 +16,7 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// Generator implements lightclient.Prover 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 diff --git a/link/internal/relay/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index 27fd66114..da004ecdb 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -1,8 +1,6 @@ // 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 ( @@ -17,13 +15,12 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// Key identifies one configured light client by the chain it lives on and -// its client id, the composite key Prover instances are scoped by. +// Key identifies a client on a chain. func Key(chainID, clientID string) string { return chainID + "/" + clientID } -// Set resolves a Prover by (chainID, clientID). +// Set resolves provers by chain and client ID. type Set struct { generators map[string]lightclient.Prover } @@ -41,9 +38,7 @@ func (s *Set) Get(chainID, clientID string) (lightclient.Prover, bool) { return generator, ok } -// NewSetFromConfig resolves a Prover for every client end of every -// configured connection. Attestation uses its built-in resolver; other client -// types dispatch through the caller-supplied registry. +// NewSetFromConfig resolves provers for all configured client ends. func NewSetFromConfig( ctx context.Context, cfg config.Config, @@ -67,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 { diff --git a/link/internal/relay/txbuilder/evm/evm.go b/link/internal/relay/txbuilder/evm/evm.go index 00202fa7e..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 lightclient.Prover.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/lightclient/params.go b/link/lightclient/params.go index bd26c7d5e..528c30536 100644 --- a/link/lightclient/params.go +++ b/link/lightclient/params.go @@ -7,18 +7,12 @@ import ( "github.com/pkg/errors" ) -// RawParams is a client-type-specific config block captured verbatim at decode -// time and interpreted by the ProverFactory registered for that type, not by the -// config package. -// -// It is used as a pointer in config structs so that adding it does not make -// those structs non-comparable. +// RawParams stores client-specific configuration for a ProverFactory. type RawParams struct { raw []byte } -// NewRawParams builds params from an already-encoded YAML document. Mainly -// useful in tests. +// NewRawParams wraps an encoded YAML document. func NewRawParams(raw []byte) *RawParams { return &RawParams{raw: append([]byte(nil), raw...)} } @@ -39,21 +33,12 @@ func (p RawParams) MarshalYAML() ([]byte, error) { return p.raw, nil } -// IsEmpty reports whether no params were configured. A ProverFactory that requires -// params should check this and return a helpful error rather than decoding an -// empty document into a zero value. +// IsEmpty reports whether params were configured. func (p *RawParams) IsEmpty() bool { return p == nil || len(p.raw) == 0 } -// Decode unmarshals the params into v, rejecting fields v does not declare. -// -// Rejecting unknown fields matters: the top-level config decode cannot see -// inside a captured block, so without this a misspelled key would silently -// become a zero value. Factories should always decode through this method -// rather than unmarshalling p.Bytes() themselves. -// -// Decoding empty params is a no-op, leaving v at its defaults. +// Decode strictly unmarshals params into v. Empty params leave v unchanged. func (p *RawParams) Decode(v any) error { if p.IsEmpty() { return nil @@ -66,8 +51,7 @@ func (p *RawParams) Decode(v any) error { return nil } -// Bytes returns the captured document. Prefer Decode, which applies strict -// field checking. +// Bytes returns the captured document. func (p *RawParams) Bytes() []byte { if p == nil { return nil diff --git a/link/lightclient/proofgen.go b/link/lightclient/proofgen.go index d96dcb015..24c5084cd 100644 --- a/link/lightclient/proofgen.go +++ b/link/lightclient/proofgen.go @@ -1,12 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// Package lightclient is the public extension point for light client proof -// generation. Implement Prover and ProverFactory here, register the factory -// under a client type name, and supply it through link/cli. -// -// This package must not import anything under link/internal. Everything it -// exposes is deliberately narrow: proofs are opaque bytes, and the relayer -// never interprets them. +// Package lightclient defines custom light-client proof generation. package lightclient import ( @@ -16,22 +10,15 @@ import ( channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" ) -// Prover generates packet membership/non-membership proofs and state -// proofs for one configured light client. +// Prover generates proofs for one light client. type Prover 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 returns the latest provable height and timestamp. LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) - // StateProof proves the light client's counterparty state at height. The - // returned bytes are passed to the light client's updateClient entrypoint - // unmodified. + // StateProof proves 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 returns one proof per packet, in packet order. PacketProofs( ctx context.Context, height uint64, @@ -40,10 +27,9 @@ type Prover interface { ) ([][]byte, error) } -// ProofKind the kind of packet claim a proof attests to. +// ProofKind identifies the packet claim being proved. type ProofKind int -// Proof kinds. const ( ProofKindUnknown ProofKind = iota ProofKindPacketCommitment diff --git a/link/lightclient/registry.go b/link/lightclient/registry.go index bf6e282b7..787b4d4ce 100644 --- a/link/lightclient/registry.go +++ b/link/lightclient/registry.go @@ -9,51 +9,42 @@ import ( "github.com/pkg/errors" ) -// ClientInfo describes one configured light-client instance from the -// perspective of the ProverFactory responsible for its type. +// ClientInfo describes a configured light client. type ClientInfo struct { ClientID string Type string ClientParams *RawParams } -// ChainInfo is the chain configuration relevant to proof generation. It omits -// operational settings, such as the deployer, that custom provers do not need. +// ChainInfo contains chain settings available to provers. type ChainInfo struct { ChainID string EVM *EVMChainInfo } -// EVMChainInfo contains the EVM connection details available to a prover. +// EVMChainInfo contains EVM chain settings. type EVMChainInfo struct { RPC string ICS26Router string } -// ProverFactoryOptions describes the configured light-client instance a -// ProverFactory constructs. Additional shared construction dependencies may -// be added here as the extension API evolves. +// ProverFactoryOptions contains inputs for constructing a prover. type ProverFactoryOptions struct { Client ClientInfo HostChain ChainInfo CounterpartyChain ChainInfo } -// ProverFactory builds proof generators for one custom light-client type. -// New validates the client parameters as part of construction. +// ProverFactory builds provers for one custom light-client type. type ProverFactory interface { - // Type returns the name operators use in connection configuration. + // Type returns the configured client type name. Type() string - // New builds a generator for Client, which tracks CounterpartyChain. + // New builds a prover for Client. New(ctx context.Context, options ProverFactoryOptions) (Prover, error) } -// Registry resolves custom light-client factories by config type name. -// Built-in client types are resolved internally by the relayer. -// -// A Registry is not safe for concurrent registration. Build it fully during -// startup, then treat it as read-only. +// Registry resolves custom light-client factories by type. type Registry struct { factories map[string]ProverFactory } @@ -62,8 +53,7 @@ func NewRegistry() *Registry { return &Registry{factories: make(map[string]ProverFactory)} } -// Register associates a factory with the client type returned by Type. -// Registering a type twice is an error rather than a silent overwrite. +// 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") @@ -95,7 +85,7 @@ func (r *Registry) Get(clientType string) (ProverFactory, bool) { return f, ok } -// Types lists every registered client type, sorted. Useful for error messages. +// Types returns the registered type names in sorted order. func (r *Registry) Types() []string { if r == nil { return nil diff --git a/link/lightclient/remotepoc/attestation_handler.go b/link/lightclient/remotepoc/attestation_handler.go index 0250bd552..877c4dda9 100644 --- a/link/lightclient/remotepoc/attestation_handler.go +++ b/link/lightclient/remotepoc/attestation_handler.go @@ -19,7 +19,7 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// NewHandler exposes generator through the proof-of-concept HTTP protocol. +// NewHandler serves a prover over HTTP. func NewHandler(generator lightclient.Prover) *http.Server { mux := http.NewServeMux() mux.HandleFunc("POST /proof", func(w http.ResponseWriter, r *http.Request) { @@ -54,8 +54,7 @@ func NewHandler(generator lightclient.Prover) *http.Server { return &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second} } -// NewAttestationHandler constructs the configured attestation generator for -// one client and exposes it through the remote light-client HTTP protocol. +// 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 { diff --git a/link/lightclient/remotepoc/remote.go b/link/lightclient/remotepoc/remote.go index 1305f6b2b..9e1b79b27 100644 --- a/link/lightclient/remotepoc/remote.go +++ b/link/lightclient/remotepoc/remote.go @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// Package remotepoc adapts a light-client proof generator to a small HTTP/JSON -// protocol. It is a proof of concept, not a production transport. +// Package remotepoc provides an experimental HTTP prover. package remotepoc import ( @@ -18,15 +17,15 @@ import ( "github.com/cosmos/ibc/link/lightclient" ) -// Type is the config name of the remote proof generator. +// Type is the remote prover's config type. const Type = "remote" -// ClientParams configure the remote proof service for one light client. +// ClientParams configures the remote service. type ClientParams struct { URL string `yaml:"url"` } -// Factory constructs remote proof generators. +// Factory constructs remote provers. type Factory struct { HTTPClient *http.Client } From d6a1e43e8e486d3faeca90a01f890653712dbc13 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 20:14:00 -0700 Subject: [PATCH 10/17] test(e2e): remove redundant custom prover fixture Signed-off-by: Dennis Fang --- e2e/cmd/custom-ibc/main.go | 4 -- e2e/custom_light_client_cli_test.go | 34 ---------- .../customlightclient/customlightclient.go | 65 ------------------- 3 files changed, 103 deletions(-) delete mode 100644 e2e/internal/customlightclient/customlightclient.go diff --git a/e2e/cmd/custom-ibc/main.go b/e2e/cmd/custom-ibc/main.go index 6a889425b..261b1a42a 100644 --- a/e2e/cmd/custom-ibc/main.go +++ b/e2e/cmd/custom-ibc/main.go @@ -5,7 +5,6 @@ package main import ( "os" - "github.com/cosmos/ibc/e2e/internal/customlightclient" "github.com/cosmos/ibc/link/cli" "github.com/cosmos/ibc/link/lightclient" "github.com/cosmos/ibc/link/lightclient/remotepoc" @@ -13,9 +12,6 @@ import ( func main() { registry := lightclient.NewRegistry() - if err := registry.Register(customlightclient.Factory{}); err != nil { - panic(err) - } if err := registry.Register(remotepoc.Factory{}); err != nil { panic(err) } diff --git a/e2e/custom_light_client_cli_test.go b/e2e/custom_light_client_cli_test.go index 097447ed3..16df2d8ef 100644 --- a/e2e/custom_light_client_cli_test.go +++ b/e2e/custom_light_client_cli_test.go @@ -16,7 +16,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/e2e/internal/customlightclient" "github.com/cosmos/ibc/e2e/internal/e2etest" "github.com/cosmos/ibc/e2e/internal/harness/environment" "github.com/cosmos/ibc/e2e/internal/harness/ibclink" @@ -24,39 +23,6 @@ import ( "github.com/cosmos/ibc/link/lightclient/remotepoc" ) -// TestCustomCompiledCLILoadsLightClient runs a registered custom client. -func TestCustomCompiledCLILoadsLightClient(t *testing.T) { - binary := buildCustomIBC(t) - t.Setenv("IBC_BIN", binary) - - spec, runtime := attestedMesh(e2etest.EVMChains( - t, e2etest.EVMRequirements{}, e2etest.ChainA, e2etest.ChainB, - )) - env := e2etest.Start(t, spec, runtime) - - marker := filepath.Join(t.TempDir(), "custom-client-created") - route := e2etest.AtoB(e2etest.ChainA, e2etest.ChainB) - driver, _ := e2etest.DeployWithRelayerConfig( - t, - env, - e2etest.NewSigner(t), - e2etest.NewSigner(t), - func(cfg *ibclink.RelayerConfig) { - require.NotEmpty(t, cfg.Connections) - cfg.Connections[0].ClientAType = customlightclient.Type - cfg.Connections[0].ClientAParams = map[string]any{"markerFile": marker} - }, - route, - ) - - relayer := e2etest.StartRelayer(t, driver, env) - require.NotEmpty(t, relayer.Ready().HTTP) - - createdFor, err := os.ReadFile(marker) - require.NoError(t, err, "custom light-client factory was not invoked") - require.NotEmpty(t, createdFor) -} - // TestRemoteAttestationLightClientRelaysPacket relays through a remote prover. func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { binary := buildCustomIBC(t) diff --git a/e2e/internal/customlightclient/customlightclient.go b/e2e/internal/customlightclient/customlightclient.go deleted file mode 100644 index dc0bef85d..000000000 --- a/e2e/internal/customlightclient/customlightclient.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Package customlightclient provides an E2E custom prover. -package customlightclient - -import ( - "context" - "errors" - "os" - "time" - - channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" - - "github.com/cosmos/ibc/link/lightclient" -) - -const Type = "e2e-custom-client" - -type Factory struct{} - -type params struct { - MarkerFile string `yaml:"markerFile"` -} - -func (Factory) Type() string { return Type } - -func (Factory) New( - _ context.Context, - options lightclient.ProverFactoryOptions, -) (lightclient.Prover, error) { - self := options.Client - var p params - if err := self.ClientParams.Decode(&p); err != nil { - return nil, err - } - if p.MarkerFile == "" { - return nil, errors.New("markerFile is required") - } - if err := os.WriteFile( - p.MarkerFile, []byte(options.HostChain.ChainID+"/"+self.ClientID), 0o600, - ); err != nil { - return nil, err - } - - return Generator{}, nil -} - -type Generator struct{} - -func (Generator) LatestProvableHeight(context.Context) (uint64, time.Time, error) { - return 0, time.Time{}, errors.New("e2e custom generator does not generate proofs") -} - -func (Generator) StateProof(context.Context, uint64) ([]byte, error) { - return nil, errors.New("e2e custom generator does not generate proofs") -} - -func (Generator) PacketProofs( - context.Context, - uint64, - lightclient.ProofKind, - []channeltypesv2.Packet, -) ([][]byte, error) { - return nil, errors.New("e2e custom generator does not generate proofs") -} From ba4319cacf6aeeadfc19ba4e6f3b7b5230643d8f Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 20:19:51 -0700 Subject: [PATCH 11/17] refactor(link): name custom prover factories explicitly Signed-off-by: Dennis Fang --- e2e/cmd/custom-ibc/main.go | 2 +- link/cli/relayer.go | 2 +- link/cli/root.go | 4 ++-- link/internal/bootstrap/bootstrap.go | 7 +++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/e2e/cmd/custom-ibc/main.go b/e2e/cmd/custom-ibc/main.go index 261b1a42a..81c2f477c 100644 --- a/e2e/cmd/custom-ibc/main.go +++ b/e2e/cmd/custom-ibc/main.go @@ -17,7 +17,7 @@ func main() { } root := cli.NewRootCmd(cli.Options{ - Relayer: cli.RelayerOptions{LightClients: registry}, + Relayer: cli.RelayerOptions{ProverFactories: registry}, }) os.Exit(cli.Execute(root)) } diff --git a/link/cli/relayer.go b/link/cli/relayer.go index 6192323ee..285218eb9 100644 --- a/link/cli/relayer.go +++ b/link/cli/relayer.go @@ -57,7 +57,7 @@ func relayerRun(cmd *cobra.Command, _ []string) error { } app, err := bootstrap.BuildRelayer(cfg, bootstrap.RelayerOptions{ - LightClients: relayerOptions.LightClients, + ProverFactories: relayerOptions.ProverFactories, }) if err != nil { return err diff --git a/link/cli/root.go b/link/cli/root.go index f50f2923e..bda29d918 100644 --- a/link/cli/root.go +++ b/link/cli/root.go @@ -25,8 +25,8 @@ type Options struct { // RelayerOptions configures relayer commands. type RelayerOptions struct { - // LightClients contains custom client types. - LightClients *lightclient.Registry + // ProverFactories contains custom prover factories. + ProverFactories *lightclient.Registry } // global globalFlags, loaded in config.DeclarePersistentFlags() diff --git a/link/internal/bootstrap/bootstrap.go b/link/internal/bootstrap/bootstrap.go index fdb3b81e8..e2fbea4f4 100644 --- a/link/internal/bootstrap/bootstrap.go +++ b/link/internal/bootstrap/bootstrap.go @@ -36,9 +36,8 @@ type Services struct { // RelayerOptions supplies optional relayer extensions. type RelayerOptions struct { - // LightClients contains custom client types. Built-in attestation clients - // are resolved internally. - LightClients *lightclient.Registry + // ProverFactories contains custom prover factories. + ProverFactories *lightclient.Registry } // BuildRelayer converts config into a runnable relayer process with all of the deps provisioned. @@ -88,7 +87,7 @@ func BuildRelayer(cfg config.Config, opts RelayerOptions) (*Services, error) { // Proof generators proofGenerators, err := proofgen.NewSetFromConfig( - ctx, cfg, clientSet, append(local, remote...), opts.LightClients, + ctx, cfg, clientSet, append(local, remote...), opts.ProverFactories, ) if err != nil { return nil, err From 6526cd462c0f2eba73bcdc6554ca4da0a1742df9 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 20:31:45 -0700 Subject: [PATCH 12/17] test(e2e): simplify remote prover setup Signed-off-by: Dennis Fang --- e2e/custom_light_client_cli_test.go | 59 ++++++++++++++++------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/e2e/custom_light_client_cli_test.go b/e2e/custom_light_client_cli_test.go index 16df2d8ef..bf7cbe173 100644 --- a/e2e/custom_light_client_cli_test.go +++ b/e2e/custom_light_client_cli_test.go @@ -25,13 +25,11 @@ import ( // TestRemoteAttestationLightClientRelaysPacket relays through a remote prover. func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { - binary := buildCustomIBC(t) - t.Setenv("IBC_BIN", binary) + t.Setenv("IBC_BIN", buildCustomIBC(t)) + ctx := t.Context() listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - t.Cleanup(func() { _ = listener.Close() }) - remoteURL := "http://" + listener.Addr().String() spec, runtime := attestedMesh(e2etest.EVMChains( t, e2etest.EVMRequirements{}, e2etest.ChainA, e2etest.ChainB, @@ -49,40 +47,47 @@ func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { require.NotEmpty(t, cfg.Connections) serviceConfig = cloneRelayerConfig(*cfg) cfg.Connections[0].ClientAType = remotepoc.Type - cfg.Connections[0].ClientAParams = map[string]any{"url": remoteURL} + cfg.Connections[0].ClientAParams = map[string]any{"url": "http://" + listener.Addr().String()} }, route, ) - - useEnvironmentRPCs(t, env, &serviceConfig) - serviceConfigPath := filepath.Join(t.TempDir(), "attestation-proof-service.yaml") - require.NoError(t, ibclink.WriteRelayerConfig(serviceConfigPath, serviceConfig)) - remoteEnd := serviceConfig.Connections[0] - server, err := remotepoc.NewAttestationHandler( - t.Context(), serviceConfigPath, remoteEnd.ChainA, remoteEnd.ClientA, - ) - require.NoError(t, err) - serverErrors := make(chan error, 1) - go func() { serverErrors <- server.Serve(listener) }() - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - require.NoError(t, server.Shutdown(ctx)) - serveErr := <-serverErrors - require.ErrorIs(t, serveErr, http.ErrServerClosed) - }) + serveAttestationProver(t, listener, env, serviceConfig) relayer := e2etest.StartRelayer(t, driver, env) transfer, err := e2etest.NewTransfer(t, env, deployment, sender, route).Send( - t.Context(), e2etest.TransferRequest{Amount: big.NewInt(1_234_000)}, + ctx, e2etest.TransferRequest{Amount: big.NewInt(1_234_000)}, ) require.NoError(t, err) - require.NoError(t, e2etest.RelayAll(t.Context(), relayer, transfer.PacketTx())) + require.NoError(t, e2etest.RelayAll(ctx, relayer, transfer.PacketTx())) _, err = e2etest.AwaitState( - t.Context(), relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED, + ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED, ) require.NoError(t, err) - require.NoError(t, transfer.VerifyDelivered(t.Context())) + 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 { From 913ad6530b0a74bf2c726733e45e9d6e06f8fa8a Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 20:34:58 -0700 Subject: [PATCH 13/17] refactor(link): use prover terminology in remote client Signed-off-by: Dennis Fang --- e2e/custom_light_client_cli_test.go | 5 ++++ link/internal/config/relayer_test.go | 2 +- link/internal/livevalidate/quorum_test.go | 2 +- link/internal/relay/proofgen/proofgen.go | 2 +- link/internal/relay/proofgen/proofgen_test.go | 9 ++++--- .../remotepoc/attestation_handler.go | 14 +++++------ link/lightclient/remotepoc/remote.go | 24 +++++++++---------- 7 files changed, 31 insertions(+), 27 deletions(-) diff --git a/e2e/custom_light_client_cli_test.go b/e2e/custom_light_client_cli_test.go index bf7cbe173..0ef58985a 100644 --- a/e2e/custom_light_client_cli_test.go +++ b/e2e/custom_light_client_cli_test.go @@ -25,6 +25,7 @@ import ( // 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() @@ -38,6 +39,8 @@ func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { 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, @@ -51,8 +54,10 @@ func TestRemoteAttestationLightClientRelaysPacket(t *testing.T) { }, 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)}, diff --git a/link/internal/config/relayer_test.go b/link/internal/config/relayer_test.go index 883af35f0..1777ef8f6 100644 --- a/link/internal/config/relayer_test.go +++ b/link/internal/config/relayer_test.go @@ -199,7 +199,7 @@ func TestRelayerConfig(t *testing.T) { }, { // An unregistered type is not a structural config error. It is - // resolved when the relayer constructs proof generators. + // resolved when the relayer constructs provers. name: "unregistered client type passes structural validation", patch: func(c *Config) { c.Relayer.Connections[0].ClientA.Type = "tendermint" diff --git a/link/internal/livevalidate/quorum_test.go b/link/internal/livevalidate/quorum_test.go index 5ff771667..b6eb8b55e 100644 --- a/link/internal/livevalidate/quorum_test.go +++ b/link/internal/livevalidate/quorum_test.go @@ -139,7 +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, `no proof generator registered for 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/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index da004ecdb..c68019b6c 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -107,7 +107,7 @@ func addGenerator( if !ok { registered := append([]string{string(config.ClientTypeAttestation)}, reg.Types()...) return errors.Errorf( - "connection %q: no proof generator registered for client type %q (registered: %v)", + "connection %q: no prover registered for client type %q (registered: %v)", connAlias, client.Type, registered, ) } diff --git a/link/internal/relay/proofgen/proofgen_test.go b/link/internal/relay/proofgen/proofgen_test.go index d9b11b17d..b39d41dda 100644 --- a/link/internal/relay/proofgen/proofgen_test.go +++ b/link/internal/relay/proofgen/proofgen_test.go @@ -81,8 +81,7 @@ 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] @@ -119,7 +118,7 @@ func TestNewSetFromConfig(t *testing.T) { _, err := NewSetFromConfig(ctx, cfg, clientSet, nil, nil) // ASSERT - require.ErrorContains(t, err, `no proof generator registered for client type "tendermint"`) + require.ErrorContains(t, err, `no prover registered for client type "tendermint"`) }) t.Run("arbitraryRegisteredClientTypeResolves", func(t *testing.T) { @@ -173,7 +172,7 @@ func (f stubFactory) New( if f.built != nil { f.built <- options } - return stubGenerator{}, nil + return stubProver{}, nil } -type stubGenerator struct{ lightclient.Prover } +type stubProver struct{ lightclient.Prover } diff --git a/link/lightclient/remotepoc/attestation_handler.go b/link/lightclient/remotepoc/attestation_handler.go index 877c4dda9..9c3d9e069 100644 --- a/link/lightclient/remotepoc/attestation_handler.go +++ b/link/lightclient/remotepoc/attestation_handler.go @@ -20,7 +20,7 @@ import ( ) // NewHandler serves a prover over HTTP. -func NewHandler(generator lightclient.Prover) *http.Server { +func NewHandler(prover lightclient.Prover) *http.Server { mux := http.NewServeMux() mux.HandleFunc("POST /proof", func(w http.ResponseWriter, r *http.Request) { var req request @@ -33,12 +33,12 @@ func NewHandler(generator lightclient.Prover) *http.Server { var err error switch req.Operation { case "latest": - res.Height, res.Timestamp, err = generator.LatestProvableHeight(r.Context()) + res.Height, res.Timestamp, err = prover.LatestProvableHeight(r.Context()) case "state": - res.Proof, err = generator.StateProof(r.Context(), req.Height) + res.Proof, err = prover.StateProof(r.Context(), req.Height) case "packets": var proofs [][]byte - proofs, err = generator.PacketProofs(r.Context(), req.Height, req.Kind, req.Packets) + proofs, err = prover.PacketProofs(r.Context(), req.Height, req.Kind, req.Packets) res.Proofs = proofs default: err = fmt.Errorf("unknown operation %q", req.Operation) @@ -78,12 +78,12 @@ func NewAttestationHandler(ctx context.Context, configPath, chainID, clientID st if !ok { return nil, errors.Errorf("client %q on chain %q is not configured", clientID, chainID) } - generator, err := attestation.ResolveGenerator( + prover, err := attestation.ResolveGenerator( ctx, self, counterparty, clients, append(local, remoteAttestors...), ) if err != nil { - return nil, errors.Wrap(err, "resolve attestation proof generator") + return nil, errors.Wrap(err, "resolve attestation prover") } - return NewHandler(generator), nil + return NewHandler(prover), nil } diff --git a/link/lightclient/remotepoc/remote.go b/link/lightclient/remotepoc/remote.go index 9e1b79b27..1d4e911e1 100644 --- a/link/lightclient/remotepoc/remote.go +++ b/link/lightclient/remotepoc/remote.go @@ -50,11 +50,11 @@ func (f Factory) New( client = &http.Client{Timeout: 30 * time.Second} } - return &Generator{url: strings.TrimRight(p.URL, "/") + "/proof", client: client}, nil + return &Prover{url: strings.TrimRight(p.URL, "/") + "/proof", client: client}, nil } -// Generator forwards proof generation to an HTTP service. -type Generator struct { +// Prover forwards proof generation to an HTTP service. +type Prover struct { url string client *http.Client } @@ -74,39 +74,39 @@ type response struct { Error string `json:"error,omitempty"` } -func (g *Generator) LatestProvableHeight(ctx context.Context) (uint64, time.Time, error) { - res, err := g.call(ctx, request{Operation: "latest"}) +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 (g *Generator) StateProof(ctx context.Context, height uint64) ([]byte, error) { - res, err := g.call(ctx, request{Operation: "state", Height: height}) +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 (g *Generator) PacketProofs( +func (p *Prover) PacketProofs( ctx context.Context, height uint64, kind lightclient.ProofKind, packets []channeltypesv2.Packet, ) ([][]byte, error) { - res, err := g.call(ctx, request{Operation: "packets", Height: height, Kind: kind, Packets: packets}) + res, err := p.call(ctx, request{Operation: "packets", Height: height, Kind: kind, Packets: packets}) return res.Proofs, err } -func (g *Generator) call(ctx context.Context, req request) (response, error) { +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, g.url, strings.NewReader(string(body))) + 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 := g.client.Do(httpReq) + httpRes, err := p.client.Do(httpReq) if err != nil { return response{}, err } From 278015978845c3299d1e89315b9c6570e7edd2f7 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 20:42:44 -0700 Subject: [PATCH 14/17] chore(e2e): remove cometbft replace comment Signed-off-by: Dennis Fang --- e2e/go.mod | 4 ---- 1 file changed, 4 deletions(-) diff --git a/e2e/go.mod b/e2e/go.mod index 0d8e4eaa6..11433fe5c 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -365,8 +365,4 @@ require ( replace github.com/cosmos/ibc/link => ../link -// Mirrors link/go.mod. Replace directives are not inherited from dependency -// modules, so importing link/cli — which compiles link's signer stack — makes -// this module resolve stock cometbft, which lacks privval.ParseNoiseAddr and -// breaks github.com/cosmos/kms/config. replace github.com/cometbft/cometbft => github.com/cometbft/cometbft v0.39.0-rc1.0.20260615134937-9ea34470f336 From d916511d340a333b65db4bde45110a6b9e9c98a3 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 21:01:45 -0700 Subject: [PATCH 15/17] docs(link): restore block header comment Signed-off-by: Dennis Fang --- link/internal/types/v2/client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/link/internal/types/v2/client.go b/link/internal/types/v2/client.go index fdb11d997..122303193 100644 --- a/link/internal/types/v2/client.go +++ b/link/internal/types/v2/client.go @@ -7,8 +7,7 @@ import ( "time" ) -// BlockHeader is the minimal view of a block the attestor and proof generators -// need: a height and its timestamp. +// BlockHeader represents a minimal subset of fields that IBC client needs for *attestation*. type BlockHeader struct { Height uint64 Timestamp time.Time From 80ac2eaae0c7968522b37dd29b013c1719ab02df Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 21:07:16 -0700 Subject: [PATCH 16/17] fix(link): document proof kind constants Signed-off-by: Dennis Fang --- link/lightclient/proofgen.go | 1 + 1 file changed, 1 insertion(+) diff --git a/link/lightclient/proofgen.go b/link/lightclient/proofgen.go index 24c5084cd..b14a2d680 100644 --- a/link/lightclient/proofgen.go +++ b/link/lightclient/proofgen.go @@ -30,6 +30,7 @@ type Prover interface { // ProofKind identifies the packet claim being proved. type ProofKind int +// Proof kinds. const ( ProofKindUnknown ProofKind = iota ProofKindPacketCommitment From 6bf4b15a3ddaa877df53e19cb1c0a043e7153a0d Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Tue, 18 Aug 2026 21:18:45 -0700 Subject: [PATCH 17/17] test(e2e): add remote prover to test matrix Signed-off-by: Dennis Fang --- e2e/test-matrix.md | 1 + 1 file changed, 1 insertion(+) 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 |