From 63505e96db44f42eddf856d84c6f8283d8d437c7 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 30 Jul 2026 11:14:07 -0400 Subject: [PATCH 01/30] Libs first --- libs/go.mod | 153 +++++- libs/go.sum | 672 +++++++++++++++++++++++++- libs/grpcutils/client_cache.go | 52 ++ libs/standalone/bootstrap_gen.go | 369 ++++++++++++++ libs/standalone/bootstrapper.go | 191 ++++++++ libs/standalone/cmd_helpers.go | 18 + libs/standalone/config_dependency.go | 31 ++ libs/standalone/db/db_dependency.go | 74 +++ libs/standalone/db/migrate.go | 49 ++ libs/standalone/gen/bootstrap.go.tmpl | 30 ++ libs/standalone/gen/main.go | 49 ++ libs/standalone/ocr/dependency.go | 222 +++++++++ libs/standalone/ocr/keyring.go | 68 +++ libs/standalone/ocr/peergroup.go | 58 +++ libs/standalone/telemetry.go | 122 +++++ 15 files changed, 2121 insertions(+), 37 deletions(-) create mode 100644 libs/grpcutils/client_cache.go create mode 100644 libs/standalone/bootstrap_gen.go create mode 100644 libs/standalone/bootstrapper.go create mode 100644 libs/standalone/cmd_helpers.go create mode 100644 libs/standalone/config_dependency.go create mode 100644 libs/standalone/db/db_dependency.go create mode 100644 libs/standalone/db/migrate.go create mode 100644 libs/standalone/gen/bootstrap.go.tmpl create mode 100644 libs/standalone/gen/main.go create mode 100644 libs/standalone/ocr/dependency.go create mode 100644 libs/standalone/ocr/keyring.go create mode 100644 libs/standalone/ocr/peergroup.go create mode 100644 libs/standalone/telemetry.go diff --git a/libs/go.mod b/libs/go.mod index e47bc7838..635e5c90c 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -7,88 +7,208 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-plugin v1.8.0 + github.com/jackc/pgx/v5 v5.9.2 + github.com/jmoiron/sqlx v1.4.0 + github.com/pressly/goose/v3 v3.27.1 + github.com/prometheus/client_golang v1.23.2 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260601182856-0b9e9346b65c - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 - github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e + github.com/smartcontractkit/chainlink-common/keystore v1.2.0 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b + github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d + github.com/smartcontractkit/libocr v0.0.0-20260508200755-99940c85383c + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.uber.org/zap v1.27.1 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 - google.golang.org/grpc v1.80.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 ) require ( + cosmossdk.io/api v0.7.6 // indirect + cosmossdk.io/collections v0.4.0 // indirect + cosmossdk.io/core v0.11.0 // indirect + cosmossdk.io/depinject v1.1.0 // indirect + cosmossdk.io/errors v1.0.1 // indirect + cosmossdk.io/log v1.4.1 // indirect + cosmossdk.io/math v1.4.0 // indirect + cosmossdk.io/store v1.1.1 // indirect + cosmossdk.io/x/tx v0.13.7 // indirect + filippo.io/bigmod v0.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + filippo.io/nistec v0.0.4 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.1 // indirect + github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/NethermindEth/juno v0.12.5 // indirect + github.com/NethermindEth/starknet.go v0.8.0 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/XSAM/otelsql v0.37.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v1.1.5 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/cometbft/cometbft v0.38.21 // indirect + github.com/cometbft/cometbft-db v0.14.1 // indirect + github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-db v1.1.1 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect + github.com/cosmos/cosmos-sdk v0.50.14 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/gogoproto v1.7.0 // indirect + github.com/cosmos/ics23/go v0.11.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dgraph-io/badger/v4 v4.2.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.7.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.17.0 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.13.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/getsentry/sentry-go v0.27.0 // indirect github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // 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.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go v1.2.8 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.3 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.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.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/linxGnu/grocksdb v1.8.14 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/chain-selectors v1.0.100 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b // indirect + github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect + github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect + github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/stellar/go-stellar-sdk v0.4.0 // indirect + github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xssnick/tonutils-go v1.14.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect + go.dedis.ch/fixbuf v1.0.3 // indirect + go.dedis.ch/kyber/v3 v3.1.0 // indirect + go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect @@ -112,11 +232,18 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + golang.org/x/tools v0.45.0 // indirect + google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect + pgregory.net/rapid v1.1.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/libs/go.sum b/libs/go.sum index 71323b09c..462cf05ba 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -1,57 +1,251 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= +cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= +cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= +cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= +cosmossdk.io/core v0.11.0 h1:vtIafqUi+1ZNAE/oxLOQQ7Oek2n4S48SWLG8h/+wdbo= +cosmossdk.io/core v0.11.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= +cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E= +cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI= +cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= +cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= +cosmossdk.io/log v1.4.1 h1:wKdjfDRbDyZRuWa8M+9nuvpVYxrEOwbD/CA8hvhU8QM= +cosmossdk.io/log v1.4.1/go.mod h1:k08v0Pyq+gCP6phvdI6RCGhLf/r425UT6Rk/m+o74rU= +cosmossdk.io/math v1.4.0 h1:XbgExXFnXmF/CccPPEto40gOO7FpWu9yWNAZPN3nkNQ= +cosmossdk.io/math v1.4.0/go.mod h1:O5PkD4apz2jZs4zqFdTr16e1dcaQCc5z6lkEnrrppuk= +cosmossdk.io/store v1.1.1 h1:NA3PioJtWDVU7cHHeyvdva5J/ggyLDkyH0hGHl2804Y= +cosmossdk.io/store v1.1.1/go.mod h1:8DwVTz83/2PSI366FERGbWSH7hL6sB7HbYp8bqksNwM= +cosmossdk.io/x/tx v0.13.7 h1:8WSk6B/OHJLYjiZeMKhq7DK7lHDMyK0UfDbBMxVmeOI= +cosmossdk.io/x/tx v0.13.7/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= +filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= +filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s= +filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR8jAwb1Ie9GyehWjVcGh32Y2MznE= +github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NethermindEth/juno v0.12.5 h1:a+KYQg8MxzNJIbbqGHq+vU9nTyuWu3acbyXxcUPUDOY= +github.com/NethermindEth/juno v0.12.5/go.mod h1:XonWmZVRwCVHv1gjoVCoTFiZnYObwdukpd3NCsl04bA= +github.com/NethermindEth/starknet.go v0.8.0 h1:mGh7qDWrvuXJPcgGJP31DpifzP6+Ef2gt/BQhaqsV40= +github.com/NethermindEth/starknet.go v0.8.0/go.mod h1:slNA8PxtxA/0LQv0FwHnL3lHFDNhVZfTK6U2gjVb7l8= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= github.com/apache/arrow-go/v18 v18.3.1/go.mod h1:12QBya5JZT6PnBihi5NJTzbACrDGXYkrgjujz3MRQXU= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +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/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= +github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 h1:nLaJZcVAnaqch3K83AyzHfY2DmQM18/L7jvkmKSfkpI= github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1/go.mod h1:6Q+F2puKpJ6zWv+R02BVnizJICf7++oRT5zwpZQAsbk= github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= +github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= +github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= +github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= +github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= +github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= +github.com/cosmos/cosmos-sdk v0.50.14 h1:G8CtGHFWbExa+ZpVOVAb4kFmko/R30igsYOwyzRMtgY= +github.com/cosmos/cosmos-sdk v0.50.14/go.mod h1:hrWEFMU1eoXqLJeE6VVESpJDQH67FS1nnMrQIjO2daw= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= +github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= +github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= +github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= +github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= +github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= +github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= +github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= +github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= +github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= +github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= +github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= +github.com/gagliardetto/solana-go v1.13.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -60,16 +254,41 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -77,11 +296,18 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -89,32 +315,98 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= +github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= +github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= +github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -125,130 +417,348 @@ github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/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/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= +github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +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= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +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= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260601182856-0b9e9346b65c h1:YSlNhm723PwpFeRbPJtZrQBbT5Mr/EpsKBXbAlsa82Y= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260601182856-0b9e9346b65c/go.mod h1:noBAXFIyadzhElKzb6a7pngnRsVTtV91muoeDqNnuUg= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0 h1:NExKM/D0HneOq/N5LGTbkV4VOa0UHCvfTNEb4GqYpto= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e h1:rg0UBvdzvZzDmWn2gURQt8+cnI7NZDhFA1dhTdZw6rQ= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= +github.com/smartcontractkit/chainlink-common/keystore v1.2.0 h1:1BH/b14CkGjArfzznlioQpIJiynECWVT48JUP9E277U= +github.com/smartcontractkit/chainlink-common/keystore v1.2.0/go.mod h1:9R/74vN+bJ5PbkOyM/pUy/AeAZaRwYb/k4XPeXcbDio= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 h1:iljEJss3WOwcsMkWy72Yn2zvjw7Gyxc+RXL7r8YKM6g= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d h1:DDoHNG+dMIixwGAQaFUORdreS/fcbw+AotZA1CcrJwY= +github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d/go.mod h1:P38krjjMclE8tCnuBB5/RhC9+ouGaRL6XXRMdw1gVBE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b h1:36knUpKHHAZ86K4FGWXtx8i/EQftGdk2bqCoEu/Cha8= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe/go.mod h1:z7lx7wI3XZ4u9kmUtAVdwn1BCC9T8aieWSDcuDgPTdQ= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 h1:/vkKPJoweLkRd56V4YHGRAtTG4+/JAlgklGEfvH6l4c= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 h1:LQy2j2+TdKLSWsUTUYuqmQPn8kjqCLjGI3ZJYGtDc08= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= -github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74= -github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= +github.com/smartcontractkit/libocr v0.0.0-20260508200755-99940c85383c h1:meDKygNIR0tdT3Xmxe9NwyuiaCCDL0a9COqZ+4cL89g= +github.com/smartcontractkit/libocr v0.0.0-20260508200755-99940c85383c/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= +github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb h1:kLHdQQkijaPGsBbtV2rJgpzVpQ96e7T10pzjNlWfK8U= +github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb/go.mod h1:4s5hj/nlMF9WV+T5Uhy4n9IYpRpzfJzT+vTKkNT7T+Y= +github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= +github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945/go.mod h1:m3pdp17i4bD50XgktkzWetcV5yaLsi7Gunbv4ZgN6qg= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stellar/go-stellar-sdk v0.4.0 h1:WBLeJk7EllU7WhwrTH1L5Mu2EhZor/xy5sVi/82JzNQ= +github.com/stellar/go-stellar-sdk v0.4.0/go.mod h1:tLKAQPxa2I5UvGMabBbUXcY3fmgYnfDudrMeK7CDX4w= +github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf h1:GY1RVbX3Hg7poPXEf6yojjP0hyypvgUgZmCqQU9D0xg= +github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf/go.mod h1:If+U9Z1W5xU97VrOgJandQT+2dN7/iOpkCrxBJEyF80= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg= +github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= +go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= +go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= +go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= +go.dedis.ch/kyber/v3 v3.1.0 h1:ghu+kiRgM5JyD9TJ0hTIxTLQlJBR/ehjWvWwYW3XsC0= +go.dedis.ch/kyber/v3 v3.1.0/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U= +go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= +go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= +go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= @@ -297,17 +807,32 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -316,56 +841,111 @@ golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5l golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -377,13 +957,19 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= @@ -391,20 +977,26 @@ gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= +google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -418,15 +1010,47 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= +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/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/libs/grpcutils/client_cache.go b/libs/grpcutils/client_cache.go new file mode 100644 index 000000000..a5f4fb29a --- /dev/null +++ b/libs/grpcutils/client_cache.go @@ -0,0 +1,52 @@ +package grpcutils + +import ( + "sync" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type ConnCache struct { + mu sync.RWMutex + conns map[string]*grpc.ClientConn +} + +func NewConnCache() *ConnCache { + return &ConnCache{conns: map[string]*grpc.ClientConn{}} +} + +func (c *ConnCache) GetConn(endpoint string) (*grpc.ClientConn, error) { + // First check with read lock + c.mu.RLock() + conn, exists := c.conns[endpoint] + c.mu.RUnlock() + + if exists { + // Check if connection is still valid + if conn.GetState().String() != "SHUTDOWN" { + return conn, nil + } + // Connection is dead, remove it and create new one + c.mu.Lock() + delete(c.conns, endpoint) + c.mu.Unlock() + } + + // Create new connection with write lock + c.mu.Lock() + defer c.mu.Unlock() + + // Double-check after acquiring write lock + conn, exists = c.conns[endpoint] + if exists { + return conn, nil + } + + conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + c.conns[endpoint] = conn + return conn, nil +} diff --git a/libs/standalone/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go new file mode 100644 index 000000000..5017633ac --- /dev/null +++ b/libs/standalone/bootstrap_gen.go @@ -0,0 +1,369 @@ +// Code generated by github.com/smartcontractkit/capabilities/libs/standalone/gen, DO NOT EDIT. + +package standalone + +import ( + "context" + + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// Run1 bootstraps the services built from 1 dependency. +// fn receives the context, the StandaloneConfig, and the 1 dependency, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run1[T0 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0]) []services.Service, + bootDep0 BootstrapDependency[T0], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0) + }) +} + +// Run2 bootstraps the services built from 2 dependencies. +// fn receives the context, the StandaloneConfig, and the 2 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run2[T0 any, T1 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1) + }) +} + +// Run3 bootstraps the services built from 3 dependencies. +// fn receives the context, the StandaloneConfig, and the 3 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run3[T0 any, T1 any, T2 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2) + }) +} + +// Run4 bootstraps the services built from 4 dependencies. +// fn receives the context, the StandaloneConfig, and the 4 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run4[T0 any, T1 any, T2 any, T3 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], + bootDep3 BootstrapDependency[T3], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + bootDep3.AddCommands(bs.root) + dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2, dep3) + }) +} + +// Run5 bootstraps the services built from 5 dependencies. +// fn receives the context, the StandaloneConfig, and the 5 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], + bootDep3 BootstrapDependency[T3], + bootDep4 BootstrapDependency[T4], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + bootDep3.AddCommands(bs.root) + dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + + bootDep4.AddCommands(bs.root) + dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4) + }) +} + +// Run6 bootstraps the services built from 6 dependencies. +// fn receives the context, the StandaloneConfig, and the 6 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], + bootDep3 BootstrapDependency[T3], + bootDep4 BootstrapDependency[T4], + bootDep5 BootstrapDependency[T5], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + bootDep3.AddCommands(bs.root) + dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + + bootDep4.AddCommands(bs.root) + dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + + bootDep5.AddCommands(bs.root) + dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5) + }) +} + +// Run7 bootstraps the services built from 7 dependencies. +// fn receives the context, the StandaloneConfig, and the 7 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], + bootDep3 BootstrapDependency[T3], + bootDep4 BootstrapDependency[T4], + bootDep5 BootstrapDependency[T5], + bootDep6 BootstrapDependency[T6], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + bootDep3.AddCommands(bs.root) + dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + + bootDep4.AddCommands(bs.root) + dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + + bootDep5.AddCommands(bs.root) + dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + + bootDep6.AddCommands(bs.root) + dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6) + }) +} + +// Run8 bootstraps the services built from 8 dependencies. +// fn receives the context, the StandaloneConfig, and the 8 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], + bootDep3 BootstrapDependency[T3], + bootDep4 BootstrapDependency[T4], + bootDep5 BootstrapDependency[T5], + bootDep6 BootstrapDependency[T6], + bootDep7 BootstrapDependency[T7], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + bootDep3.AddCommands(bs.root) + dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + + bootDep4.AddCommands(bs.root) + dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + + bootDep5.AddCommands(bs.root) + dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + + bootDep6.AddCommands(bs.root) + dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + + bootDep7.AddCommands(bs.root) + dep7 := &dependency[T7]{bd: bootDep7, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7) + }) +} + +// Run9 bootstraps the services built from 9 dependencies. +// fn receives the context, the StandaloneConfig, and the 9 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7], dep8 Dependency[T8]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], + bootDep3 BootstrapDependency[T3], + bootDep4 BootstrapDependency[T4], + bootDep5 BootstrapDependency[T5], + bootDep6 BootstrapDependency[T6], + bootDep7 BootstrapDependency[T7], + bootDep8 BootstrapDependency[T8], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + bootDep3.AddCommands(bs.root) + dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + + bootDep4.AddCommands(bs.root) + dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + + bootDep5.AddCommands(bs.root) + dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + + bootDep6.AddCommands(bs.root) + dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + + bootDep7.AddCommands(bs.root) + dep7 := &dependency[T7]{bd: bootDep7, bs: bs} + + bootDep8.AddCommands(bs.root) + dep8 := &dependency[T8]{bd: bootDep8, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) + }) +} + +// Run10 bootstraps the services built from 10 dependencies. +// fn receives the context, the StandaloneConfig, and the 10 dependencies, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7], dep8 Dependency[T8], dep9 Dependency[T9]) []services.Service, + bootDep0 BootstrapDependency[T0], + bootDep1 BootstrapDependency[T1], + bootDep2 BootstrapDependency[T2], + bootDep3 BootstrapDependency[T3], + bootDep4 BootstrapDependency[T4], + bootDep5 BootstrapDependency[T5], + bootDep6 BootstrapDependency[T6], + bootDep7 BootstrapDependency[T7], + bootDep8 BootstrapDependency[T8], + bootDep9 BootstrapDependency[T9], +) error { + + bootDep0.AddCommands(bs.root) + dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + + bootDep1.AddCommands(bs.root) + dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + + bootDep2.AddCommands(bs.root) + dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + + bootDep3.AddCommands(bs.root) + dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + + bootDep4.AddCommands(bs.root) + dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + + bootDep5.AddCommands(bs.root) + dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + + bootDep6.AddCommands(bs.root) + dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + + bootDep7.AddCommands(bs.root) + dep7 := &dependency[T7]{bd: bootDep7, bs: bs} + + bootDep8.AddCommands(bs.root) + dep8 := &dependency[T8]{bd: bootDep8, bs: bs} + + bootDep9.AddCommands(bs.root) + dep9 := &dependency[T9]{bd: bootDep9, bs: bs} + + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8, dep9) + }) +} diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go new file mode 100644 index 000000000..46d388d5f --- /dev/null +++ b/libs/standalone/bootstrapper.go @@ -0,0 +1,191 @@ +package standalone + +//go:generate go run ./gen + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/hashicorp/go-plugin" + "github.com/spf13/cobra" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// StandaloneConfig holds the process-wide dependencies the Bootstrapper provides +// to the service factory passed to Run. +type StandaloneConfig struct { + // Logger is hclog-compatible like a LOOP plugin's: JSON on stderr with + // @level/@message/@timestamp keys, so a go-plugin host (e.g. the core node) + // can parse and re-level the entries when this process runs under one, + // while remaining plain JSON logs when run standalone. + Logger logger.SugaredLogger +} + +type Bootstrapper struct { + root *cobra.Command + config *StandaloneConfig + commonConfig CommonConfig + beholderClient *beholder.Client // nil unless CL_TELEMETRY_ENDPOINT is configured +} + +// NewBootstrapper creates a new Bootstrapper using the cobra command as its root. +// Note that the RunE on the cobra command will be overwritten when Run is called, and the cobra command is provided only for the remaining fields. +// +// It creates the hclog-compatible logger and, when CL_TELEMETRY_* env vars are +// configured, starts beholder telemetry with any otel views from opts; it exits +// on failure. The logger runs and supervises the services (health, lifecycle +// logging) and is available via Config for use before Run. +func NewBootstrapper(root *cobra.Command, opts ...Option) *Bootstrapper { + var s settings + for _, opt := range opts { + opt(&s) + } + + lggr, err := newLogger() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to create logger: %s\n", err) + os.Exit(1) + } + slggr := logger.Sugared(logger.Named(lggr, root.Name())) + + beholderClient, err := startTelemetry(context.Background(), s.otelViews) + if err != nil { + slggr.Fatalf("Failed to start telemetry: %s", err) + } + + bs := &Bootstrapper{root: root, config: &StandaloneConfig{Logger: slggr}, beholderClient: beholderClient} + root.PersistentFlags().BoolVar(&bs.commonConfig.Fake, "fake", false, "use fake dependencies instead of real ones") + return bs +} + +// close flushes telemetry and logs: the counterpart to the setup in NewBootstrapper. +func (b *Bootstrapper) close() { + if b.beholderClient != nil { + b.config.Logger.ErrorIfFn(b.beholderClient.Close, "Failed to close beholder client") + } + _ = b.config.Logger.Sync() +} + +// Logger returns the logger instance. It is safe to call before running the binary +func (b *Bootstrapper) Logger() logger.SugaredLogger { return b.config.Logger } + +// run composes the services returned by factory into a single supervising +// service via services.Engine sub-services, so their health is aggregated the +// same way the rest of the stack does it (services.Config.NewSubServices + +// HealthReport). It starts them, then blocks until an interrupt, then closes. +func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service) error { + defer b.close() + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + b.root.RunE = func(cmd *cobra.Command, args []string) error { + svcs := factory(ctx) + root, _ := services.Config{ + Name: "Bootstrap", + NewSubServices: func(logger.Logger) []services.Service { return svcs }, + }.NewServiceEngine(b.config.Logger) + + if err := root.Start(ctx); err != nil { + stop() + return err + } + defer func() { _ = root.Close() }() + + if underPluginHost() { + // Launched by a go-plugin host (e.g. the core node): expose the empty + // LOOP so the host can supervise this process over gRPC (handshake + + // go-plugin's liveness health check). The started services run in + // this process, so that liveness reflects them. Blocks until the host + // shuts us down. + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: loop.EmptyHandshakeConfig(), + Plugins: map[string]plugin.Plugin{loop.PluginEmptyName: &loop.EmptyLoop{}}, + GRPCServer: plugin.DefaultGRPCServer, + }) + return nil + } + + // Standalone: block until interrupted, then close. + <-ctx.Done() + return nil + } + + return b.root.Execute() +} + +// underPluginHost reports whether this process was launched by a go-plugin host, +// detected via the empty plugin's handshake magic cookie. go-plugin's Serve +// refuses to run (and exits) when this is absent, so we only serve the plugin in +// that case and otherwise run standalone. +func underPluginHost() bool { + h := loop.EmptyHandshakeConfig() + return os.Getenv(h.MagicCookieKey) == h.MagicCookieValue +} + +type CommonConfig struct { + Fake bool +} + +type Dependency[T any] interface { + Get(ctx context.Context) (T, error) +} + +type BootstrapCommand interface { + AddCommands(*cobra.Command) +} + +type BootstrapDependency[T any] interface { + Get(ctx context.Context, c CommonConfig) (T, error) + BootstrapCommand +} + +type dependency[T any] struct { + bs *Bootstrapper + bd BootstrapDependency[T] +} + +func (d dependency[T]) Get(ctx context.Context) (T, error) { + return d.bd.Get(ctx, d.bs.commonConfig) +} + +// OnceBootstrapper wraps a BootstrapDependency so that Get is evaluated at most +// once: the first call resolves the dependency and caches its (value, error), +// and every subsequent call returns that same result without re-running Get +// (the ctx and CommonConfig of later calls are ignored). AddCommands is +// delegated unchanged. +// +// BootstrapDependency implementations are shared and may have Get called more +// than once — e.g. one dependency resolving another, or the same dependency +// feeding several services — so a New function should wrap its dependency with +// OnceBootstrapper before returning it, making repeated Get calls safe and +// side-effect-free. +func OnceBootstrapper[T any](bd BootstrapDependency[T]) BootstrapDependency[T] { + return &onceBootstrapper[T]{bd: bd} +} + +type onceBootstrapper[T any] struct { + bd BootstrapDependency[T] + once sync.Once + val T + err error +} + +func (o *onceBootstrapper[T]) Get(ctx context.Context, c CommonConfig) (T, error) { + o.once.Do(func() { + o.val, o.err = o.bd.Get(ctx, c) + }) + return o.val, o.err +} + +func (o *onceBootstrapper[T]) AddCommands(cmd *cobra.Command) { + o.bd.AddCommands(cmd) +} diff --git a/libs/standalone/cmd_helpers.go b/libs/standalone/cmd_helpers.go new file mode 100644 index 000000000..4962be7be --- /dev/null +++ b/libs/standalone/cmd_helpers.go @@ -0,0 +1,18 @@ +package standalone + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func BindWithEnvVar(flag *pflag.Flag) { + formatted := strings.ToUpper(strings.ReplaceAll(flag.Name, "-", "_")) + creEnv := "CRE_" + formatted + clEnv := "CL_" + formatted + flag.Usage += fmt.Sprintf("[env in order: %s, %s]", creEnv, clEnv) + _ = viper.BindPFlag(flag.Name, flag) + _ = viper.BindEnv(flag.Name, creEnv, clEnv) +} diff --git a/libs/standalone/config_dependency.go b/libs/standalone/config_dependency.go new file mode 100644 index 000000000..1576c366b --- /dev/null +++ b/libs/standalone/config_dependency.go @@ -0,0 +1,31 @@ +package standalone + +import ( + "context" + "os" + + "github.com/spf13/cobra" +) + +type configDependency[T any] struct { + parse func([]byte) (T, error) + file string +} + +func NewConfigDependency[T any](parse func([]byte) (T, error)) BootstrapDependency[T] { + return OnceBootstrapper[T](&configDependency[T]{parse: parse}) +} + +func (c *configDependency[T]) Get(_ context.Context, _ CommonConfig) (T, error) { + content, err := os.ReadFile(c.file) + if err != nil { + var t T + return t, err + } + + return c.parse(content) +} + +func (c *configDependency[T]) AddCommands(command *cobra.Command) { + command.PersistentFlags().StringVar(&c.file, "config-file", "", "specifies the config file to load from") +} diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go new file mode 100644 index 000000000..e4da157ae --- /dev/null +++ b/libs/standalone/db/db_dependency.go @@ -0,0 +1,74 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "io/fs" + "os" + + // Register the pgx database/sql driver under the name "pgx". + _ "github.com/jackc/pgx/v5/stdlib" + + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/spf13/cobra" +) + +const dbURLEnvVar = "CL_DATABASE_URL" + +func Dependency(migrationsFS fs.FS, migrationTable string) standalone.BootstrapDependency[*sql.DB] { + // Wrap in OnceBootstrapper so Get (which opens the DB and runs migrations) + // runs at most once even if several services resolve this dependency. + return standalone.OnceBootstrapper[*sql.DB](&dependency{migrationsFS: migrationsFS, migrationTable: migrationTable}) +} + +type dependency struct { + db *sql.DB + url string + useRealDbForFake bool + migrationsFS fs.FS + migrationTable string +} + +func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConfig) (*sql.DB, error) { + dbFn := pgDb + if commonConfig.Fake && !d.useRealDbForFake { + // TODO set db to an in-memory one + // Also add a subcommand to override with real DB even if fake is used + } + + var err error + d.db, err = dbFn() + if err != nil { + return nil, err + } + + if err = migrate(ctx, d.db, d.migrationsFS, d.migrationTable); err != nil { + return nil, err + } + + return d.db, nil +} + +func (d *dependency) AddCommands(command *cobra.Command) { + f := command.PersistentFlags() + f.BoolVar(&d.useRealDbForFake, "real-db", false, "uses a real db even if fake is set for the program") + f.StringVar(&d.url, "db-url", "", "database url") + standalone.BindWithEnvVar(f.Lookup("db-url")) +} + +func (d *dependency) Close() { + _ = d.db.Close() +} + +func pgDb() (*sql.DB, error) { + + dbURL := os.Getenv(dbURLEnvVar) + if dbURL == "" { + return nil, fmt.Errorf("%s must be set", dbURLEnvVar) + } + + return sql.Open("pgx", dbURL) +} + +var _ standalone.BootstrapDependency[*sql.DB] = (*dependency)(nil) diff --git a/libs/standalone/db/migrate.go b/libs/standalone/db/migrate.go new file mode 100644 index 000000000..c04a9142e --- /dev/null +++ b/libs/standalone/db/migrate.go @@ -0,0 +1,49 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "io/fs" + "log" + + "github.com/pressly/goose/v3" + "github.com/pressly/goose/v3/database" +) + +// migrate applies all pending migrations found in migrationsFS to db. +// +// migrationsFS must be rooted at the directory containing the goose `.sql` +// migration files (e.g. the result of fs.Sub on an embedded filesystem). +// +// migrationsTable is the name of the table goose uses to track which +// migrations have been applied. Pass a binary-specific name so multiple +// schemas can coexist in the same database without clobbering each other's +// migration history. +func migrate(ctx context.Context, db *sql.DB, migrationsFS fs.FS, migrationsTable string) error { + migrationsFS, err := fs.Sub(migrationsFS, "migrations") + if err != nil { + return err + } + + store, err := database.NewStore(goose.DialectPostgres, migrationsTable) + if err != nil { + return fmt.Errorf("failed to create goose store: %w", err) + } + + // Go migrations are registered globally in goose; reset before building the + // provider so repeated calls (e.g. in tests) don't accumulate duplicates. + // See: https://github.com/pressly/goose/issues/782 + goose.ResetGlobalMigrations() + + p, err := goose.NewProvider("", db, migrationsFS, goose.WithStore(store)) + if err != nil { + return fmt.Errorf("failed to create goose provider: %w", err) + } + + if _, err = p.Up(ctx); err != nil { + return fmt.Errorf("failed to apply migrations: %w", err) + } + log.Printf("migrations applied (history table %q)", migrationsTable) + return nil +} diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl new file mode 100644 index 000000000..cc0c9af00 --- /dev/null +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -0,0 +1,30 @@ +package standalone + +import ( + "context" + + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +{{- range . }} + +// Run{{.}} bootstraps the services built from {{.}} {{if eq . 1}}dependency{{else}}dependencies{{end}}. +// fn receives the context, the StandaloneConfig, and the {{.}} {{if eq . 1}}dependency{{else}}dependencies{{end}}, and returns the services to run. +// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// The returned services are started together and their health is aggregated by the bootstrapper. +func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, {{range RangeNum .}}dep{{.}} Dependency[T{{.}}], {{end}}) []services.Service, + {{- range RangeNum .}} + bootDep{{.}} BootstrapDependency[T{{.}}], + {{- end}} +) error { + {{range RangeNum .}} + bootDep{{.}}.AddCommands(bs.root) + dep{{.}} := &dependency[T{{.}}]{bd: bootDep{{.}}, bs: bs} + {{end}} + return bs.run(func(ctx context.Context) []services.Service { + return fn(ctx, bs.config, {{range RangeNum .}}dep{{.}}, {{end}}) + }) +} +{{- end }} diff --git a/libs/standalone/gen/main.go b/libs/standalone/gen/main.go new file mode 100644 index 000000000..f7ff2511b --- /dev/null +++ b/libs/standalone/gen/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + _ "embed" + "log" + "text/template" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/codegen" +) + +//go:embed bootstrap.go.tmpl +var bootstrapGo string + +const toolName = "github.com/smartcontractkit/capabilities/libs/standalone/gen" + +// maxDeps is the highest number of dependencies a generated Run helper accepts. +// Run1..Run{maxDeps} are generated. +const maxDeps = 10 + +func main() { + // rangeNum(maxDeps+1)[1:] yields 1..maxDeps so we skip the zero-dependency case. + nums := rangeNum(maxDeps + 1)[1:] + + t, err := template.New("bootstrap").Funcs(template.FuncMap{"RangeNum": rangeNum}).Parse(bootstrapGo) + if err != nil { + log.Fatal(err) + } + + results := bytes.Buffer{} + if err = t.Execute(&results, nums); err != nil { + log.Fatal(err) + } + + files := map[string]string{"bootstrap_gen.go": results.String()} + if err = codegen.WriteFiles(".", "github.com/smartcontractkit", toolName, files); err != nil { + log.Fatal(err) + } +} + +// rangeNum returns a slice [0, 1, ..., num-1], used by the template to iterate type parameters and arguments. +func rangeNum(num int) []int { + nums := make([]int, num) + for i := range num { + nums[i] = i + } + + return nums +} diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go new file mode 100644 index 000000000..0e4fc7cb9 --- /dev/null +++ b/libs/standalone/ocr/dependency.go @@ -0,0 +1,222 @@ +// Package ocr provides a standalone.BootstrapDependency that supplies the +// libocr rage networking factories (OCR endpoint, OCR3.1 endpoint, and +// DON-to-DON peer group) a standalone binary needs. +// +// It has two mutually exclusive modes, mirroring core's SingletonPeerWrapper: +// +// - create: build a local libocr peer (networking.NewPeer) and expose its +// factories. Requires --listen-addresses and uses the node's P2P identity +// and OCR discoverer table from the database. +// - proxy: delegate rage networking to an out-of-process proxy at +// --proxy-address, exposing proxy-client-backed factories instead of a +// local peer. +// +// The two modes are wired as a cobra "one of" set: exactly one of +// --listen-addresses / --proxy-address may (and must) be provided. +package ocr + +import ( + "context" + "database/sql" + "errors" + "fmt" + "io" + "time" + + "github.com/jmoiron/sqlx" + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + + "github.com/smartcontractkit/libocr/networking" + "github.com/smartcontractkit/libocr/networking/rageping" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonocr "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +// Factories bundles the libocr rage networking factories the caller serves or +// drives. It is produced by either the create or proxy mode; the caller does +// not need to know which. Close tears down the underlying peer (create mode) or +// proxy client connections (proxy mode). +type Factories struct { + // OCR2Endpoint creates OCR2 BinaryNetworkEndpoints. + OCR2Endpoint ocr2types.BinaryNetworkEndpointFactory + // OCR3_1Endpoint creates OCR3.1 BinaryNetworkEndpoint2s. + OCR3_1Endpoint ocr2types.BinaryNetworkEndpoint2Factory + // PeerGroup creates DON-to-DON peer groups. + PeerGroup creproxy.PeerGroupFactory + + closer io.Closer +} + +// Close releases the underlying peer or proxy clients. +func (f *Factories) Close() error { + if f == nil || f.closer == nil { + return nil + } + return f.closer.Close() +} + +// Dependency returns a standalone.BootstrapDependency that resolves the libocr +// Factories. It wraps the database dependency, which it uses to load the node's +// P2P identity (both modes) and the OCR discoverer database (create mode). +// discovererTable is the migration-created table backing p2p announcements. +func Dependency(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*Factories] { + // Wrap in OnceBootstrapper so Get (which creates the peer or proxy clients) + // runs at most once even if several services resolve this dependency. + return standalone.OnceBootstrapper[*Factories](&dependency{lggr: lggr, db: db, discovererTable: discovererTable}) +} + +type dependency struct { + lggr commonlogger.Logger + db standalone.BootstrapDependency[*sql.DB] + discovererTable string + + // create-mode config + listenAddresses []string + announceAddresses []string + deltaReconcile time.Duration + deltaDial time.Duration + incomingBufferSize int + outgoingBufferSize int + + // proxy-mode config + proxyAddress string +} + +var _ standalone.BootstrapDependency[*Factories] = (*dependency)(nil) + +func (d *dependency) AddCommands(cmd *cobra.Command) { + // The database is a create/proxy-shared input (P2P identity, discoverer + // table), so surface its flags too. + d.db.AddCommands(cmd) + + f := cmd.PersistentFlags() + // create-mode flags + f.StringSliceVar(&d.listenAddresses, "listen-addresses", nil, "rage p2p V2 listen addresses (host:port); creates a local peer") + f.StringSliceVar(&d.announceAddresses, "announce-addresses", nil, "rage p2p V2 announce addresses (host:port); defaults to the listen addresses") + f.DurationVar(&d.deltaReconcile, "delta-reconcile", time.Minute, "rage p2p V2 delta reconcile interval") + f.DurationVar(&d.deltaDial, "delta-dial", 5*time.Second, "rage p2p V2 minimum interval between dial attempts") + f.IntVar(&d.incomingBufferSize, "incoming-buffer-size", 100, "per-remote incoming message buffer size") + f.IntVar(&d.outgoingBufferSize, "outgoing-buffer-size", 100, "per-remote outgoing message buffer size") + // proxy-mode flag + f.StringVar(&d.proxyAddress, "proxy-address", "", "delegate rage networking to a proxy at this gRPC address instead of creating a local peer") + + // Exactly one mode: --listen-addresses (create) xor --proxy-address (proxy). + cmd.MarkFlagsMutuallyExclusive("listen-addresses", "proxy-address") + cmd.MarkFlagsOneRequired("listen-addresses", "proxy-address") + // announce-addresses is optional in create mode (libocr defaults it to the + // listen addresses) but is meaningless in proxy mode. + cmd.MarkFlagsMutuallyExclusive("announce-addresses", "proxy-address") +} + +func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Factories, error) { + sqlDB, err := d.db.Get(ctx, cc) + if err != nil { + return nil, fmt.Errorf("failed to get database: %w", err) + } + ds := sqlx.NewDb(sqlDB, "pgx") + + // Both modes use the node's own P2P identity so this process is the same + // peer as the node it fronts. + keyring, err := loadPeerKeyring(ctx, ds) + if err != nil { + return nil, err + } + peerID := ragetypes.PeerIDFromKeyring(keyring) + + if d.proxyAddress != "" { + return d.proxyFactories(peerID) + } + return d.localFactories(ds, keyring, peerID) +} + +// localFactories builds a real libocr peer and exposes its factories. +func (d *dependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*Factories, error) { + if len(d.listenAddresses) == 0 { + return nil, errors.New("at least one --listen-addresses is required") + } + + discovererDB := commonocr.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) + + d.lggr.Infow("Creating local p2p peer", + "peerID", peerID.String(), + "listenAddresses", d.listenAddresses, + "announceAddresses", d.announceAddresses, + ) + + peer, err := networking.NewPeer(networking.PeerConfig{ + PeerKeyring: keyring, + Logger: commonlogger.NewOCRWrapper(d.lggr, false, func(string) {}), + V2ListenAddresses: d.listenAddresses, + V2AnnounceAddresses: d.announceAddresses, + V2DeltaReconcile: d.deltaReconcile, + V2DeltaDial: d.deltaDial, + V2DiscovererDatabase: discovererDB, + V2EndpointConfig: networking.EndpointConfigV2{ + IncomingMessageBufferSize: d.incomingBufferSize, + OutgoingMessageBufferSize: d.outgoingBufferSize, + }, + MetricsRegisterer: prometheus.DefaultRegisterer, + LatencyMetricsServiceConfigs: rageping.DefaultConfigs(), + }) + if err != nil { + return nil, fmt.Errorf("failed to create rage peer: %w", err) + } + + return &Factories{ + OCR2Endpoint: peer.OCR2BinaryNetworkEndpointFactory(), + OCR3_1Endpoint: peer.OCR3_1BinaryNetworkEndpointFactory(), + PeerGroup: newNetworkingPeerGroupFactory(peer.PeerGroupFactory()), + closer: peer, + }, nil +} + +// proxyFactories delegates rage networking to an out-of-process proxy: no local +// peer is created; the factories are backed by proxy clients connected to +// d.proxyAddress. The node's raw peer ID is passed to the endpoint factories, +// as libocr compares it against the peer IDs in the OCR config. +func (d *dependency) proxyFactories(peerID ragetypes.PeerID) (*Factories, error) { + endpointFactory, err := creproxy.NewProxyEndpointFactory(peerID.String(), d.proxyAddress) + if err != nil { + return nil, fmt.Errorf("failed to create proxy OCR endpoint factory: %w", err) + } + endpoint2Factory, err := creproxy.NewProxyEndpoint2Factory(peerID.String(), d.proxyAddress) + if err != nil { + _ = endpointFactory.Close() + return nil, fmt.Errorf("failed to create proxy OCR3.1 endpoint factory: %w", err) + } + pgFactory, err := creproxy.NewProxyPeerGroupFactory(d.proxyAddress) + if err != nil { + _ = endpointFactory.Close() + _ = endpoint2Factory.Close() + return nil, fmt.Errorf("failed to create proxy peer group factory: %w", err) + } + + d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.proxyAddress, "peerID", peerID.String()) + + return &Factories{ + OCR2Endpoint: endpointFactory, + OCR3_1Endpoint: endpoint2Factory, + PeerGroup: pgFactory, + closer: multiCloser{endpointFactory, endpoint2Factory, pgFactory}, + }, nil +} + +// multiCloser closes several io.Closers, returning the first error. +type multiCloser []io.Closer + +func (m multiCloser) Close() error { + var err error + for _, c := range m { + if cerr := c.Close(); cerr != nil && err == nil { + err = cerr + } + } + return err +} diff --git a/libs/standalone/ocr/keyring.go b/libs/standalone/ocr/keyring.go new file mode 100644 index 000000000..78a600279 --- /dev/null +++ b/libs/standalone/ocr/keyring.go @@ -0,0 +1,68 @@ +package ocr + +import ( + "context" + "crypto" + "crypto/rand" + "errors" + "fmt" + "os" + + "github.com/jmoiron/sqlx" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/models" +) + +// keystorePasswordEnvVar is the keystore password used to decrypt the node's +// key ring. This process shares the node's DB (CL_DATABASE_URL) and password. +const keystorePasswordEnvVar = "CL_PASSWORD_KEYSTORE" + +// loadPeerKeyring loads the P2P key from the node's keystore so this process +// uses the SAME peer identity as the node it fronts (other DON members expect +// this node's peer ID at this address). It reads the node's existing encrypted +// key ring (the legacy `encrypted_key_rings` table, in chainlink-common's +// corekeys/models format) and decrypts it with the keystore password. This is a +// deliberately small copy of core's keyManager.Unlock using only +// chainlink-common packages, so this binary needn't import chainlink core. +// +// TODO: drop this once the keystore is migrated to chainlink-common's +// keystore.Keystore + pgstore (as chainlink-ccv already uses), after which we +// can LoadKeystore from the shared table directly. +func loadPeerKeyring(ctx context.Context, ds *sqlx.DB) (*peerKeyring, error) { + var encrypted []byte + if err := ds.GetContext(ctx, &encrypted, "SELECT encrypted_keys FROM encrypted_key_rings LIMIT 1"); err != nil { + return nil, fmt.Errorf("failed to read node key ring: %w", err) + } + kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(os.Getenv(keystorePasswordEnvVar)) + if err != nil { + return nil, fmt.Errorf("failed to decrypt node key ring: %w", err) + } + for _, k := range kr.P2P { + pub, perr := ragetypes.PeerPublicKeyFromGenericPublicKey(k.Public()) + if perr != nil { + return nil, fmt.Errorf("failed to derive peer public key: %w", perr) + } + return &peerKeyring{signer: k, publicKey: pub}, nil + } + return nil, errors.New("no P2P key found in node key ring") +} + +// peerKeyring is a ragetypes.PeerKeyring backed by the node's P2P key (a +// crypto.Signer), used in place of the deprecated PeerConfig.PrivKey field. +type peerKeyring struct { + signer crypto.Signer + publicKey ragetypes.PeerPublicKey +} + +var _ ragetypes.PeerKeyring = (*peerKeyring)(nil) + +// Sign returns an EdDSA-Ed25519 signature over msg, as required by PeerKeyring. +func (k *peerKeyring) Sign(msg []byte) ([]byte, error) { + return k.signer.Sign(rand.Reader, msg, crypto.Hash(0)) +} + +func (k *peerKeyring) PublicKey() ragetypes.PeerPublicKey { + return k.publicKey +} diff --git a/libs/standalone/ocr/peergroup.go b/libs/standalone/ocr/peergroup.go new file mode 100644 index 000000000..4ceea6d07 --- /dev/null +++ b/libs/standalone/ocr/peergroup.go @@ -0,0 +1,58 @@ +package ocr + +import ( + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/networking" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/libocr/ragep2p" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// newNetworkingPeerGroupFactory adapts a libocr networking.PeerGroupFactory +// (from a local peer) to the creproxy.PeerGroupFactory the proxy server serves. +// The method sets are structurally identical; this only bridges the types so +// create and proxy modes both yield a creproxy.PeerGroupFactory. +func newNetworkingPeerGroupFactory(inner networking.PeerGroupFactory) creproxy.PeerGroupFactory { + return networkingPeerGroupFactory{inner: inner} +} + +type networkingPeerGroupFactory struct { + inner networking.PeerGroupFactory +} + +func (a networkingPeerGroupFactory) NewPeerGroup(configDigest [32]byte, peerIDs []string, bootstrappers []creproxy.BootstrapperInfo) (creproxy.PeerGroup, error) { + locators := make([]commontypes.BootstrapperLocator, len(bootstrappers)) + for i, b := range bootstrappers { + locators[i] = commontypes.BootstrapperLocator{PeerID: b.PeerID, Addrs: b.Addrs} + } + pg, err := a.inner.NewPeerGroup(ocr2types.ConfigDigest(configDigest), peerIDs, locators) + if err != nil { + return nil, err + } + return networkingPeerGroup{inner: pg}, nil +} + +type networkingPeerGroup struct { + inner networking.PeerGroup +} + +func (a networkingPeerGroup) NewStream(remotePeerID string, args creproxy.StreamArgs) (creproxy.PeerGroupStream, error) { + st, err := a.inner.NewStream(remotePeerID, networking.NewStreamArgs1{ + StreamName: args.StreamName, + OutgoingBufferSize: args.OutgoingBufferSize, + IncomingBufferSize: args.IncomingBufferSize, + MaxMessageLength: args.MaxMessageLength, + MessagesLimit: ragep2p.TokenBucketParams{Rate: args.MessagesLimit.Rate, Capacity: args.MessagesLimit.Capacity}, + BytesLimit: ragep2p.TokenBucketParams{Rate: args.BytesLimit.Rate, Capacity: args.BytesLimit.Capacity}, + }) + if err != nil { + return nil, err + } + // networking.Stream's method set matches creproxy.PeerGroupStream. + return st, nil +} + +func (a networkingPeerGroup) Close() error { + return a.inner.Close() +} diff --git a/libs/standalone/telemetry.go b/libs/standalone/telemetry.go new file mode 100644 index 000000000..30d7b4799 --- /dev/null +++ b/libs/standalone/telemetry.go @@ -0,0 +1,122 @@ +package standalone + +// This file copies the two small pieces of LOOP plugin setup that standalone +// binaries also need — the hclog-compatible logger and the CL_TELEMETRY_* env +// config for beholder — so that they behave the same with or without a plugin +// host, without depending on loop.Server and the full env contract it requires. + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// Env vars mirroring the loop.EnvConfig telemetry subset, set by the plugin +// host (or by the operator, when running standalone). +const ( + envTelemetryEndpoint = "CL_TELEMETRY_ENDPOINT" + envTelemetryInsecureConn = "CL_TELEMETRY_INSECURE_CONNECTION" + envTelemetryCACertFile = "CL_TELEMETRY_CA_CERT_FILE" + envTelemetryAttribute = "CL_TELEMETRY_ATTRIBUTE_" + envTelemetryAuthHeader = "CL_TELEMETRY_AUTH_HEADER" + envTelemetryAuthPubKeyHex = "CL_TELEMETRY_AUTH_PUB_KEY_HEX" +) + +// Option configures optional Bootstrapper behavior. +type Option func(*settings) + +type settings struct { + otelViews []sdkmetric.View +} + +// WithOtelViews sets otel metric views (e.g. histogram bucket boundaries) on +// the beholder client. Views only apply to instruments created after +// NewBootstrapper, since aggregation is fixed when the client is created. +func WithOtelViews(otelViews []sdkmetric.View) Option { + return func(s *settings) { s.otelViews = append(s.otelViews, otelViews...) } +} + +// newLogger returns a logger encoding hclog-compatible JSON on stderr, like a +// LOOP plugin's: a go-plugin host parses and re-levels these entries, while +// standalone they are plain zap JSON logs. Level is Debug because filtering is +// the reader's job (host-side, or the log pipeline). +func newLogger() (logger.Logger, error) { + return logger.NewWith(func(cfg *zap.Config) { + cfg.Level.SetLevel(zap.DebugLevel) + cfg.EncoderConfig.LevelKey = "@level" + cfg.EncoderConfig.MessageKey = "@message" + cfg.EncoderConfig.TimeKey = "@timestamp" + cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02T15:04:05.000000Z07:00") + }) +} + +// startTelemetry creates, starts, and installs the process-global beholder +// client from the CL_TELEMETRY_* env vars, so instruments created afterwards +// via beholder.GetMeter report over OTLP. When no endpoint is configured it +// returns nil and the global noop client stays: instruments record nothing. +func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder.Client, error) { + endpoint := os.Getenv(envTelemetryEndpoint) + if endpoint == "" { + return nil, nil + } + + cfg := beholder.DefaultConfig() + cfg.OtelExporterGRPCEndpoint = endpoint + cfg.InsecureConnection = false + if s := os.Getenv(envTelemetryInsecureConn); s != "" { + var err error + cfg.InsecureConnection, err = strconv.ParseBool(s) + if err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", envTelemetryInsecureConn, err) + } + } + cfg.CACertFile = os.Getenv(envTelemetryCACertFile) + for k, v := range envMap(envTelemetryAttribute) { + cfg.ResourceAttributes = append(cfg.ResourceAttributes, attribute.String(k, v)) + } + cfg.AuthHeaders = envMap(envTelemetryAuthHeader) + cfg.AuthPublicKeyHex = os.Getenv(envTelemetryAuthPubKeyHex) + // Logs already reach their destination via stderr (parsed by the plugin + // host when under one); don't stream them a second time. + cfg.LogStreamingEnabled = false + // Per the OTEL specification, histogram buckets must be defined when the + // client is created, so the views cannot be applied any later than this. + cfg.MetricViews = otelViews + + client, err := beholder.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create beholder client: %w", err) + } + if err := client.Start(ctx); err != nil { + return nil, fmt.Errorf("failed to start beholder client: %w", err) + } + beholder.SetClient(client) + beholder.SetGlobalOtelProviders() + return client, nil +} + +// envMap collects env vars starting with prefix into a map, with the prefix +// stripped from the keys, mirroring how the plugin host encodes map-valued +// config (loop.EnvConfig.AsCmdEnv). Returns nil when none are set. +func envMap(prefix string) map[string]string { + var m map[string]string + for _, env := range os.Environ() { + if key, value, found := strings.Cut(env, "="); found && strings.HasPrefix(key, prefix) { + if m == nil { + m = make(map[string]string) + } + m[strings.TrimPrefix(key, prefix)] = value + } + } + return m +} From 4bfc82e7f773ef10db49b7273bcc45a1c195740f Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 30 Jul 2026 14:17:07 -0400 Subject: [PATCH 02/30] PSB for rage proxying --- crecore/endpoint2_server.go | 235 ++++ crecore/go.mod | 252 ++++ crecore/go.sum | 1052 +++++++++++++++++ crecore/main.go | 61 + crecore/metrics.go | 76 ++ ...ate_proxy_ocr_discoverer_announcements.sql | 12 + crecore/peergroup_server.go | 201 ++++ crecore/proxy_service.go | 113 ++ crecore/server.go | 144 +++ 9 files changed, 2146 insertions(+) create mode 100644 crecore/endpoint2_server.go create mode 100644 crecore/go.mod create mode 100644 crecore/go.sum create mode 100644 crecore/main.go create mode 100644 crecore/metrics.go create mode 100644 crecore/migrations/0001_create_proxy_ocr_discoverer_announcements.sql create mode 100644 crecore/peergroup_server.go create mode 100644 crecore/proxy_service.go create mode 100644 crecore/server.go diff --git a/crecore/endpoint2_server.go b/crecore/endpoint2_server.go new file mode 100644 index 000000000..6fa04b888 --- /dev/null +++ b/crecore/endpoint2_server.go @@ -0,0 +1,235 @@ +package main + +import ( + "fmt" + "io" + "sync" + "time" + + "github.com/smartcontractkit/libocr/commontypes" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// maxPendingRequestHandles bounds the number of inbound request handles held +// per endpoint connection. Requests are single-use with expiries; if a client +// never responds, the oldest handles are evicted to avoid unbounded growth. +const maxPendingRequestHandles = 8192 + +// Endpoint2Server implements the Endpoint2Proxy gRPC service (OCR3.1). It is +// backed by a real libocr BinaryNetworkEndpoint2Factory and exposes it over the +// network, analogous to Server for OCR2 endpoints. +// +// Inbound requests carry a stateful libocr RequestHandle that cannot cross the +// wire; the server retains each handle keyed by a request id and the client +// responds with that id. +type Endpoint2Server struct { + creproxy.UnimplementedEndpoint2ProxyServer + + factory ocr2types.BinaryNetworkEndpoint2Factory + inboundSizes sizeRecorder + outboundSizes sizeRecorder +} + +// NewEndpoint2Server returns an Endpoint2Server serving endpoints created by +// the given factory, typically networking.NewPeer(...).OCR3_1BinaryNetworkEndpointFactory(). +func NewEndpoint2Server(factory ocr2types.BinaryNetworkEndpoint2Factory, metrics *proxyMetrics) *Endpoint2Server { + return &Endpoint2Server{ + factory: factory, + inboundSizes: metrics.sizes(endpointOCR3_1, directionInbound), + outboundSizes: metrics.sizes(endpointOCR3_1, directionOutbound), + } +} + +func (s *Endpoint2Server) Connect(stream creproxy.Endpoint2Proxy_ConnectServer) error { + req, err := stream.Recv() + if err != nil { + return fmt.Errorf("failed to receive initial NewEndpoint2Request: %w", err) + } + newReq, ok := req.Message.(*creproxy.Endpoint2ClientRequest_NewEndpoint) + if !ok { + return fmt.Errorf("first message must be NewEndpoint2Request, got %T", req.Message) + } + + endpoint, err := s.newEndpoint(newReq.NewEndpoint) + if err != nil { + return fmt.Errorf("failed to create endpoint2: %w", err) + } + defer func() { _ = endpoint.Close() }() + + c := &endpoint2Conn{stream: stream, handles: map[uint64]ocr2types.RequestHandle{}} + + ctx := stream.Context() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for msg := range endpoint.Receive() { + pb := c.inboundToPB(msg) + s.inboundSizes.record(ctx, len(pb.Payload)) + if err := c.send(pb); err != nil { + return + } + } + }() + defer wg.Wait() + + for { + req, err := stream.Recv() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + switch m := req.Message.(type) { + case *creproxy.Endpoint2ClientRequest_NewEndpoint: + return fmt.Errorf("NewEndpoint2Request not allowed after initial setup") + case *creproxy.Endpoint2ClientRequest_SendTo: + if out, ok := c.pbToOutbound(m.SendTo.Msg); ok { + s.outboundSizes.record(ctx, len(m.SendTo.Msg.Payload)) + endpoint.SendTo(out, commontypes.OracleID(m.SendTo.ToOracleId)) + } + case *creproxy.Endpoint2ClientRequest_Broadcast: + if out, ok := c.pbToOutbound(m.Broadcast.Msg); ok { + s.outboundSizes.record(ctx, len(m.Broadcast.Msg.Payload)) + endpoint.Broadcast(out) + } + } + } +} + +func (s *Endpoint2Server) newEndpoint(req *creproxy.NewEndpoint2Request) (ocr2types.BinaryNetworkEndpoint2, error) { + if len(req.ConfigDigest) != len(ocr2types.ConfigDigest{}) { + return nil, fmt.Errorf("invalid config digest length: got %d, expected %d", len(req.ConfigDigest), len(ocr2types.ConfigDigest{})) + } + var cd ocr2types.ConfigDigest + copy(cd[:], req.ConfigDigest) + + bootstrappers := make([]commontypes.BootstrapperLocator, len(req.V2Bootstrappers)) + for i, b := range req.V2Bootstrappers { + bootstrappers[i] = commontypes.BootstrapperLocator{PeerID: b.PeerId, Addrs: b.Addrs} + } + + return s.factory.NewEndpoint(cd, req.PeerIds, bootstrappers, + endpoint2ConfigFromPB(req.DefaultPriorityConfig), + endpoint2ConfigFromPB(req.LowPriorityConfig), + ) +} + +func endpoint2ConfigFromPB(pb *creproxy.Endpoint2Config) ocr2types.BinaryNetworkEndpoint2Config { + var c ocr2types.BinaryNetworkEndpoint2Config + if pb == nil { + return c + } + if l := pb.Limits; l != nil { + c.BinaryNetworkEndpointLimits = ocr2types.BinaryNetworkEndpointLimits{ + MaxMessageLength: int(l.MaxMessageLength), + MessagesRatePerOracle: l.MessagesRatePerOracle, + MessagesCapacityPerOracle: int(l.MessagesCapacityPerOracle), + BytesRatePerOracle: l.BytesRatePerOracle, + BytesCapacityPerOracle: int(l.BytesCapacityPerOracle), + } + } + if pb.OverrideIncomingMessageBufferSize != nil { + v := int(*pb.OverrideIncomingMessageBufferSize) + c.OverrideIncomingMessageBufferSize = &v + } + if pb.OverrideOutgoingMessageBufferSize != nil { + v := int(*pb.OverrideOutgoingMessageBufferSize) + c.OverrideOutgoingMessageBufferSize = &v + } + return c +} + +// endpoint2Conn holds per-connection state: the gRPC stream and the retained +// inbound request handles. +type endpoint2Conn struct { + stream creproxy.Endpoint2Proxy_ConnectServer + + sendMu sync.Mutex + + mu sync.Mutex + handles map[uint64]ocr2types.RequestHandle + order []uint64 // insertion order, for bounded eviction + nextID uint64 +} + +func (c *endpoint2Conn) send(m *creproxy.Endpoint2ServerMessage) error { + c.sendMu.Lock() + defer c.sendMu.Unlock() + return c.stream.Send(m) +} + +func (c *endpoint2Conn) storeHandle(h ocr2types.RequestHandle) uint64 { + c.mu.Lock() + defer c.mu.Unlock() + c.nextID++ + id := c.nextID + c.handles[id] = h + c.order = append(c.order, id) + if len(c.order) > maxPendingRequestHandles { + delete(c.handles, c.order[0]) + c.order = c.order[1:] + } + return id +} + +func (c *endpoint2Conn) popHandle(id uint64) (ocr2types.RequestHandle, bool) { + c.mu.Lock() + defer c.mu.Unlock() + h, ok := c.handles[id] + if ok { + delete(c.handles, id) + } + return h, ok +} + +func (c *endpoint2Conn) inboundToPB(msg ocr2types.InboundBinaryMessageWithSender) *creproxy.Endpoint2ServerMessage { + out := &creproxy.Endpoint2ServerMessage{Sender: uint32(msg.Sender)} + switch m := msg.InboundBinaryMessage.(type) { + case ocr2types.InboundBinaryMessagePlain: + out.Payload, out.Priority = m.Payload, uint32(m.Priority) + out.Kind = &creproxy.Endpoint2ServerMessage_Plain{Plain: &creproxy.InboundPlain2{}} + case ocr2types.InboundBinaryMessageRequest: + out.Payload, out.Priority = m.Payload, uint32(m.Priority) + out.Kind = &creproxy.Endpoint2ServerMessage_Request{ + Request: &creproxy.InboundRequest2{RequestId: c.storeHandle(m.RequestHandle)}, + } + case ocr2types.InboundBinaryMessageResponse: + out.Payload, out.Priority = m.Payload, uint32(m.Priority) + out.Kind = &creproxy.Endpoint2ServerMessage_Response{Response: &creproxy.InboundResponse2{}} + } + return out +} + +func (c *endpoint2Conn) pbToOutbound(msg *creproxy.OutboundMessage2) (ocr2types.OutboundBinaryMessage, bool) { + if msg == nil { + return nil, false + } + priority := ocr2types.BinaryMessageOutboundPriority(msg.Priority) + switch k := msg.Kind.(type) { + case *creproxy.OutboundMessage2_Plain: + return ocr2types.OutboundBinaryMessagePlain{Payload: msg.Payload, Priority: priority}, true + case *creproxy.OutboundMessage2_Request: + return ocr2types.OutboundBinaryMessageRequest{ + ResponsePolicy: ocr2types.SingleUseSizedLimitedResponsePolicy{ + MaxSize: int(k.Request.PolicyMaxSize), + ExpiryTimestamp: time.UnixMilli(k.Request.PolicyExpiryUnixMs), + }, + Payload: msg.Payload, + Priority: priority, + }, true + case *creproxy.OutboundMessage2_Response: + h, ok := c.popHandle(k.Response.RequestId) + if !ok { + // Handle unknown (evicted or already used): drop the response. + return nil, false + } + return ocr2types.MustMakeOutboundBinaryMessageResponse(h, msg.Payload, priority), true + default: + return nil, false + } +} diff --git a/crecore/go.mod b/crecore/go.mod new file mode 100644 index 000000000..aa96f5f1b --- /dev/null +++ b/crecore/go.mod @@ -0,0 +1,252 @@ +module github.com/smartcontractkit/capabilities/crecore + +go 1.26.2 + +require ( + github.com/smartcontractkit/capabilities/libs v0.0.0-20260730175407-a6d1af3f36c8 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e + github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d + github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd + github.com/spf13/cobra v1.10.2 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/metric v1.43.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 + google.golang.org/grpc v1.82.1 +) + +require ( + cosmossdk.io/api v0.7.6 // indirect + cosmossdk.io/collections v0.4.0 // indirect + cosmossdk.io/core v0.11.0 // indirect + cosmossdk.io/depinject v1.1.0 // indirect + cosmossdk.io/errors v1.0.1 // indirect + cosmossdk.io/log v1.4.1 // indirect + cosmossdk.io/math v1.4.0 // indirect + cosmossdk.io/store v1.1.1 // indirect + cosmossdk.io/x/tx v0.13.7 // indirect + filippo.io/bigmod v0.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + filippo.io/nistec v0.0.4 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.1 // indirect + github.com/DataDog/zstd v1.5.7 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/NethermindEth/juno v0.12.5 // indirect + github.com/NethermindEth/starknet.go v0.8.0 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/XSAM/otelsql v0.37.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect + github.com/bits-and-blooms/bitset v1.24.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/cockroachdb/errors v1.12.0 // indirect + github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect + github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect + github.com/cockroachdb/pebble v1.1.5 // indirect + github.com/cockroachdb/redact v1.1.6 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect + github.com/cometbft/cometbft v0.38.21 // indirect + github.com/cometbft/cometbft-db v0.14.1 // indirect + github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-db v1.1.1 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect + github.com/cosmos/cosmos-sdk v0.50.14 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/gogoproto v1.7.0 // indirect + github.com/cosmos/ics23/go v0.11.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dgraph-io/badger/v4 v4.2.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.7.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.13.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // 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.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grafana/otel-profiling-go v0.5.1 // indirect + github.com/grafana/pyroscope-go v1.2.8 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.3 // indirect + github.com/hashicorp/go-plugin v1.8.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.13.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.9.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/linxGnu/grocksdb v1.8.14 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect + github.com/oklog/run v1.2.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/zerolog v1.33.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/scylladb/go-reflectx v1.0.1 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect + github.com/smartcontractkit/chain-selectors v1.0.100 // indirect + github.com/smartcontractkit/chainlink-common/keystore v1.2.0 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b // indirect + github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect + github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect + github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect + github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect + github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect + github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/stellar/go-stellar-sdk v0.4.0 // indirect + github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xssnick/tonutils-go v1.14.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect + go.dedis.ch/fixbuf v1.0.3 // indirect + go.dedis.ch/kyber/v3 v3.1.0 // indirect + go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect + pgregory.net/rapid v1.1.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/crecore/go.sum b/crecore/go.sum new file mode 100644 index 000000000..81c66b293 --- /dev/null +++ b/crecore/go.sum @@ -0,0 +1,1052 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= +cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= +cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= +cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= +cosmossdk.io/core v0.11.0 h1:vtIafqUi+1ZNAE/oxLOQQ7Oek2n4S48SWLG8h/+wdbo= +cosmossdk.io/core v0.11.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= +cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E= +cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI= +cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= +cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= +cosmossdk.io/log v1.4.1 h1:wKdjfDRbDyZRuWa8M+9nuvpVYxrEOwbD/CA8hvhU8QM= +cosmossdk.io/log v1.4.1/go.mod h1:k08v0Pyq+gCP6phvdI6RCGhLf/r425UT6Rk/m+o74rU= +cosmossdk.io/math v1.4.0 h1:XbgExXFnXmF/CccPPEto40gOO7FpWu9yWNAZPN3nkNQ= +cosmossdk.io/math v1.4.0/go.mod h1:O5PkD4apz2jZs4zqFdTr16e1dcaQCc5z6lkEnrrppuk= +cosmossdk.io/store v1.1.1 h1:NA3PioJtWDVU7cHHeyvdva5J/ggyLDkyH0hGHl2804Y= +cosmossdk.io/store v1.1.1/go.mod h1:8DwVTz83/2PSI366FERGbWSH7hL6sB7HbYp8bqksNwM= +cosmossdk.io/x/tx v0.13.7 h1:8WSk6B/OHJLYjiZeMKhq7DK7lHDMyK0UfDbBMxVmeOI= +cosmossdk.io/x/tx v0.13.7/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= +filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= +filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s= +filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NethermindEth/juno v0.12.5 h1:a+KYQg8MxzNJIbbqGHq+vU9nTyuWu3acbyXxcUPUDOY= +github.com/NethermindEth/juno v0.12.5/go.mod h1:XonWmZVRwCVHv1gjoVCoTFiZnYObwdukpd3NCsl04bA= +github.com/NethermindEth/starknet.go v0.8.0 h1:mGh7qDWrvuXJPcgGJP31DpifzP6+Ef2gt/BQhaqsV40= +github.com/NethermindEth/starknet.go v0.8.0/go.mod h1:slNA8PxtxA/0LQv0FwHnL3lHFDNhVZfTK6U2gjVb7l8= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= +github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= +github.com/apache/arrow-go/v18 v18.3.1/go.mod h1:12QBya5JZT6PnBihi5NJTzbACrDGXYkrgjujz3MRQXU= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= +github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +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/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= +github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 h1:nLaJZcVAnaqch3K83AyzHfY2DmQM18/L7jvkmKSfkpI= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1/go.mod h1:6Q+F2puKpJ6zWv+R02BVnizJICf7++oRT5zwpZQAsbk= +github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= +github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= +github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +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/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= +github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= +github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= +github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= +github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= +github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= +github.com/cosmos/cosmos-sdk v0.50.14 h1:G8CtGHFWbExa+ZpVOVAb4kFmko/R30igsYOwyzRMtgY= +github.com/cosmos/cosmos-sdk v0.50.14/go.mod h1:hrWEFMU1eoXqLJeE6VVESpJDQH67FS1nnMrQIjO2daw= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= +github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= +github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= +github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= +github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= +github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= +github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= +github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= +github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= +github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= +github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= +github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= +github.com/gagliardetto/solana-go v1.13.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= +github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= +github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= +github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= +github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= +github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= +github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +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.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/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/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= +github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +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= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +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= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= +github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260730175407-a6d1af3f36c8 h1:KhwjNOJdP4v2dQxge/d85KH+Kry0klBtbNEOj2EjkUk= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260730175407-a6d1af3f36c8/go.mod h1:hGshOtcToWcu2Fai1p6mg+/ynKE3pKFHHRfoSiYLs7I= +github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= +github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e h1:rg0UBvdzvZzDmWn2gURQt8+cnI7NZDhFA1dhTdZw6rQ= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= +github.com/smartcontractkit/chainlink-common/keystore v1.2.0 h1:1BH/b14CkGjArfzznlioQpIJiynECWVT48JUP9E277U= +github.com/smartcontractkit/chainlink-common/keystore v1.2.0/go.mod h1:9R/74vN+bJ5PbkOyM/pUy/AeAZaRwYb/k4XPeXcbDio= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d h1:DDoHNG+dMIixwGAQaFUORdreS/fcbw+AotZA1CcrJwY= +github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d/go.mod h1:P38krjjMclE8tCnuBB5/RhC9+ouGaRL6XXRMdw1gVBE= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe/go.mod h1:z7lx7wI3XZ4u9kmUtAVdwn1BCC9T8aieWSDcuDgPTdQ= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 h1:/vkKPJoweLkRd56V4YHGRAtTG4+/JAlgklGEfvH6l4c= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= +github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= +github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd h1:ksFjz3ytjK4kH5HFHpLKzDS0/9gmeSuvii1rs8FlxrI= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= +github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb h1:kLHdQQkijaPGsBbtV2rJgpzVpQ96e7T10pzjNlWfK8U= +github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb/go.mod h1:4s5hj/nlMF9WV+T5Uhy4n9IYpRpzfJzT+vTKkNT7T+Y= +github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= +github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945/go.mod h1:m3pdp17i4bD50XgktkzWetcV5yaLsi7Gunbv4ZgN6qg= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stellar/go-stellar-sdk v0.4.0 h1:WBLeJk7EllU7WhwrTH1L5Mu2EhZor/xy5sVi/82JzNQ= +github.com/stellar/go-stellar-sdk v0.4.0/go.mod h1:tLKAQPxa2I5UvGMabBbUXcY3fmgYnfDudrMeK7CDX4w= +github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf h1:GY1RVbX3Hg7poPXEf6yojjP0hyypvgUgZmCqQU9D0xg= +github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf/go.mod h1:If+U9Z1W5xU97VrOgJandQT+2dN7/iOpkCrxBJEyF80= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg= +github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= +go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= +go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= +go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= +go.dedis.ch/kyber/v3 v3.1.0 h1:ghu+kiRgM5JyD9TJ0hTIxTLQlJBR/ehjWvWwYW3XsC0= +go.dedis.ch/kyber/v3 v3.1.0/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U= +go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= +go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= +go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= +go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= +google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= +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/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/crecore/main.go b/crecore/main.go new file mode 100644 index 000000000..dac8799d4 --- /dev/null +++ b/crecore/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "embed" + "log" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/capabilities/libs/standalone/db" + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +//go:embed migrations/*.sql +var embeddedMigrations embed.FS + +const migrationsTable = "proxy_migrations" + +// ocrDiscovererTable is the table backing OCR p2p announcements. Must match the +// CREATE TABLE in migrations/0001_*.sql. +const ocrDiscovererTable = "proxy_ocr_discoverer_announcements" + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + cfg := &Config{} + + root := &cobra.Command{ + Use: "main", + Short: "P2P proxy for the CRE", + Long: `Runs a single shared rage (libocr) peer and exposes it over gRPC so that +core can delegate its OCR networking (and, in future, DON-to-DON networking) to +this process. The peer's identity is loaded from the node's keystore (shared DB +via CL_DATABASE_URL, decrypted with CL_PASSWORD_KEYSTORE). + +Provide exactly one networking mode: --listen-addresses to run a local libocr +peer, or --proxy-address to delegate to another proxy.`, + } + + root.PersistentFlags().StringVar(&cfg.ProxyListenAddress, "proxy-listen-address", ":50051", "address the proxy gRPC server listens on") + + bootstrapper := standalone.NewBootstrapper(root, standalone.WithOtelViews(metricViews())) + lggr := bootstrapper.Logger() + + // The ocr dependency owns the libocr networking config (create vs proxy + // mode) and wraps the database dependency it needs for the P2P identity and + // OCR discoverer table. + dbDep := db.Dependency(embeddedMigrations, migrationsTable) + ocrDep := ocr.Dependency(lggr.Named("OCR"), dbDep, ocrDiscovererTable) + + return standalone.Run1(bootstrapper, func(ctx context.Context, scfg *standalone.StandaloneConfig, factories standalone.Dependency[*ocr.Factories]) []services.Service { + return []services.Service{newProxyService(cfg, scfg.Logger.Named("proxy service"), factories)} + }, ocrDep) +} diff --git a/crecore/metrics.go b/crecore/metrics.go new file mode 100644 index 000000000..316657efc --- /dev/null +++ b/crecore/metrics.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" +) + +// Attribute values for the endpoint kind and message direction on proxy metrics. +const ( + endpointOCR2 = "ocr2" + endpointOCR3_1 = "ocr3_1" + endpointPeerGroup = "peergroup" + + directionInbound = "inbound" + directionOutbound = "outbound" +) + +// proxyMetrics holds the otel instruments for the p2p proxy. +type proxyMetrics struct { + messageSize metric.Int64Histogram +} + +// newProxyMetrics creates the proxy instruments. It must be called after the +// bootstrapper has started (which configures the beholder client, including the +// views from metricViews), or the instruments bind to the noop meter forever. +func newProxyMetrics() (*proxyMetrics, error) { + messageSize, err := beholder.GetMeter().Int64Histogram( + "p2p_proxy_message_size_bytes", + metric.WithDescription("Size in bytes of message payloads relayed by the p2p proxy"), + metric.WithUnit("By"), + ) + if err != nil { + return nil, fmt.Errorf("failed to create message size histogram: %w", err) + } + return &proxyMetrics{messageSize: messageSize}, nil +} + +// sizeRecorder records payload sizes for one endpoint kind and direction, with +// the attribute set precomputed so the per-message path does not allocate. +type sizeRecorder struct { + h metric.Int64Histogram + attrs metric.MeasurementOption +} + +func (m *proxyMetrics) sizes(endpoint, direction string) sizeRecorder { + return sizeRecorder{h: m.messageSize, attrs: metric.WithAttributeSet(attribute.NewSet( + attribute.String("endpoint", endpoint), + attribute.String("direction", direction), + ))} +} + +func (r sizeRecorder) record(ctx context.Context, sizeBytes int) { + r.h.Record(ctx, int64(sizeBytes), r.attrs) +} + +// metricViews returns the otel views for the proxy's instruments. Histogram +// bucket boundaries can only be set via views when the beholder client is +// created, so these are passed to the bootstrapper via standalone.WithOtelViews. +func metricViews() []sdkmetric.View { + return []sdkmetric.View{ + sdkmetric.NewView( + sdkmetric.Instrument{Name: "p2p_proxy_message_size_bytes"}, + sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{ + // Payloads range from tiny heartbeats to multi-MB reports; the + // default otel buckets top out at 10k and would flatten that. + Boundaries: []float64{0, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000}, + }}, + ), + } +} diff --git a/crecore/migrations/0001_create_proxy_ocr_discoverer_announcements.sql b/crecore/migrations/0001_create_proxy_ocr_discoverer_announcements.sql new file mode 100644 index 000000000..b25b4f4e5 --- /dev/null +++ b/crecore/migrations/0001_create_proxy_ocr_discoverer_announcements.sql @@ -0,0 +1,12 @@ +-- +goose Up +-- announcements table for RageP2P discovery, used by DiscovererDatabase. +CREATE TABLE proxy_ocr_discoverer_announcements ( + local_peer_id text NOT NULL, + remote_peer_id text NOT NULL, + ann bytea NOT NULL, + created_at timestamptz not null, + updated_at timestamptz not null, + PRIMARY KEY(local_peer_id, remote_peer_id) +); +-- +goose Down +DROP TABLE proxy_ocr_discoverer_announcements; diff --git a/crecore/peergroup_server.go b/crecore/peergroup_server.go new file mode 100644 index 000000000..018818add --- /dev/null +++ b/crecore/peergroup_server.go @@ -0,0 +1,201 @@ +package main + +import ( + "fmt" + "io" + "sync" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// configDigestLen is the byte length of an OCR config digest. +const configDigestLen = 32 + +// PeerGroupServer implements the PeerGroupProxy gRPC service. It is backed by a +// creproxy.PeerGroupFactory (the proxy adapts a real libocr +// networking.PeerGroupFactory to this interface) and exposes DON-to-DON peer +// groups and their streams over the network. +// +// Each Connect stream owns exactly one PeerGroup: the first message must be a +// NewPeerGroupRequest, after which streams are multiplexed over the connection +// by stream_id. Closing the connection closes the group and all its streams. +type PeerGroupServer struct { + creproxy.UnimplementedPeerGroupProxyServer + + pgFactory creproxy.PeerGroupFactory + inboundSizes sizeRecorder + outboundSizes sizeRecorder +} + +// NewPeerGroupServer returns a PeerGroupServer serving groups created by the +// given factory. +func NewPeerGroupServer(pgFactory creproxy.PeerGroupFactory, metrics *proxyMetrics) *PeerGroupServer { + return &PeerGroupServer{ + pgFactory: pgFactory, + inboundSizes: metrics.sizes(endpointPeerGroup, directionInbound), + outboundSizes: metrics.sizes(endpointPeerGroup, directionOutbound), + } +} + +func (s *PeerGroupServer) Connect(stream creproxy.PeerGroupProxy_ConnectServer) error { + req, err := stream.Recv() + if err != nil { + return fmt.Errorf("failed to receive initial NewPeerGroupRequest: %w", err) + } + + newGroupReq, ok := req.Message.(*creproxy.PeerGroupClientRequest_NewPeerGroup) + if !ok { + return fmt.Errorf("first message must be NewPeerGroupRequest, got %T", req.Message) + } + + group, err := s.newPeerGroup(newGroupReq.NewPeerGroup) + if err != nil { + return fmt.Errorf("failed to create peer group: %w", err) + } + + c := &peerGroupConn{ + stream: stream, + group: group, + streams: map[string]creproxy.PeerGroupStream{}, + inboundSizes: s.inboundSizes, + outboundSizes: s.outboundSizes, + } + defer c.close() + + for { + req, err := stream.Recv() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + switch msg := req.Message.(type) { + case *creproxy.PeerGroupClientRequest_NewPeerGroup: + return fmt.Errorf("NewPeerGroupRequest not allowed after initial setup") + case *creproxy.PeerGroupClientRequest_NewStream: + if err := c.newStream(msg.NewStream); err != nil { + return fmt.Errorf("failed to create stream: %w", err) + } + case *creproxy.PeerGroupClientRequest_StreamSend: + c.sendTo(msg.StreamSend.StreamId, msg.StreamSend.Payload) + case *creproxy.PeerGroupClientRequest_CloseStream: + c.closeStream(msg.CloseStream.StreamId) + } + } +} + +func (s *PeerGroupServer) newPeerGroup(req *creproxy.NewPeerGroupRequest) (creproxy.PeerGroup, error) { + if len(req.ConfigDigest) != configDigestLen { + return nil, fmt.Errorf("invalid config digest length: got %d, expected %d", len(req.ConfigDigest), configDigestLen) + } + var configDigest [configDigestLen]byte + copy(configDigest[:], req.ConfigDigest) + + bootstrappers := make([]creproxy.BootstrapperInfo, len(req.V2Bootstrappers)) + for i, b := range req.V2Bootstrappers { + bootstrappers[i] = creproxy.BootstrapperInfo{ + PeerID: b.PeerId, + Addrs: b.Addrs, + } + } + + return s.pgFactory.NewPeerGroup(configDigest, req.PeerIds, bootstrappers) +} + +// peerGroupConn holds the per-connection state for a single PeerGroup. +type peerGroupConn struct { + stream creproxy.PeerGroupProxy_ConnectServer + group creproxy.PeerGroup + inboundSizes sizeRecorder + outboundSizes sizeRecorder + + // sendMu serializes sends on the gRPC stream, which is written to by one + // goroutine per stream's receive loop. + sendMu sync.Mutex + + mu sync.Mutex + streams map[string]creproxy.PeerGroupStream + wg sync.WaitGroup +} + +func (c *peerGroupConn) send(m *creproxy.PeerGroupServerMessage) error { + c.sendMu.Lock() + defer c.sendMu.Unlock() + return c.stream.Send(m) +} + +func (c *peerGroupConn) newStream(req *creproxy.NewStreamRequest) error { + st, err := c.group.NewStream(req.RemotePeerId, creproxy.StreamArgs{ + StreamName: req.StreamName, + OutgoingBufferSize: int(req.OutgoingBufferSize), + IncomingBufferSize: int(req.IncomingBufferSize), + MaxMessageLength: int(req.MaxMessageLength), + MessagesLimit: rateLimit(req.MessagesLimit), + BytesLimit: rateLimit(req.BytesLimit), + }) + if err != nil { + return err + } + + c.mu.Lock() + c.streams[req.StreamId] = st + c.mu.Unlock() + + streamID := req.StreamId + recv := st.ReceiveMessages() + c.wg.Add(1) + go func() { + defer c.wg.Done() + for payload := range recv { + c.inboundSizes.record(c.stream.Context(), len(payload)) + if err := c.send(&creproxy.PeerGroupServerMessage{ + Message: &creproxy.PeerGroupServerMessage_StreamRecv{ + StreamRecv: &creproxy.StreamRecv{StreamId: streamID, Payload: payload}, + }, + }); err != nil { + return + } + } + }() + return nil +} + +func (c *peerGroupConn) sendTo(streamID string, payload []byte) { + c.mu.Lock() + st := c.streams[streamID] + c.mu.Unlock() + if st != nil { + c.outboundSizes.record(c.stream.Context(), len(payload)) + st.SendMessage(payload) + } +} + +func (c *peerGroupConn) closeStream(streamID string) { + c.mu.Lock() + st := c.streams[streamID] + delete(c.streams, streamID) + c.mu.Unlock() + if st != nil { + _ = st.Close() + } +} + +func (c *peerGroupConn) close() { + c.mu.Lock() + for id, st := range c.streams { + _ = st.Close() + delete(c.streams, id) + } + c.mu.Unlock() + _ = c.group.Close() + c.wg.Wait() +} + +func rateLimit(p *creproxy.TokenBucketParams) creproxy.RateLimit { + if p == nil { + return creproxy.RateLimit{} + } + return creproxy.RateLimit{Rate: p.Rate, Capacity: p.Capacity} +} diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go new file mode 100644 index 000000000..c3c876553 --- /dev/null +++ b/crecore/proxy_service.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "errors" + "fmt" + "net" + + "google.golang.org/grpc" + + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// Config is the proxy gRPC server configuration, populated from CLI flags (see +// main.go). The libocr peer / proxy-client configuration lives on the ocr +// bootstrap dependency instead. +type Config struct { + // ProxyListenAddress is the address the proxy gRPC server listens on. + ProxyListenAddress string +} + +// proxyService exposes the libocr rage networking factories over gRPC so that +// core can delegate its OCR networking (and, in future, DON-to-DON networking) +// to this process. The factories come from the ocr bootstrap dependency, which +// either creates a local peer or is backed by another proxy. +// +// NOTE: per the standalone framework, Start blocks until shutdown (the +// Bootstrapper returns the result of Start directly). +type proxyService struct { + services.Service + eng *services.Engine + + cfg *Config + lggr logger.Logger + factories standalone.Dependency[*ocr.Factories] + + grpcServer *grpc.Server + factoriesClose func() error +} + +var _ services.Service = (*proxyService)(nil) + +// newProxyService builds the proxy service using the standard +// services.Config/Engine pattern, so its lifecycle and health integrate with +// the bootstrapper's aggregated health report. +func newProxyService(cfg *Config, lggr logger.Logger, factories standalone.Dependency[*ocr.Factories]) *proxyService { + s := &proxyService{cfg: cfg, lggr: lggr, factories: factories} + s.Service, s.eng = services.Config{ + Name: "P2PProxy", + Start: s.start, + Close: s.close, + }.NewServiceEngine(lggr) + return s +} + +func (s *proxyService) start(ctx context.Context) error { + if s.cfg.ProxyListenAddress == "" { + return errors.New("--proxy-listen-address is required") + } + + factories, err := s.factories.Get(ctx) + if err != nil { + return fmt.Errorf("failed to get libocr factories: %w", err) + } + s.factoriesClose = factories.Close + + metrics, err := newProxyMetrics() + if err != nil { + return fmt.Errorf("failed to create proxy metrics: %w", err) + } + + lis, err := net.Listen("tcp", s.cfg.ProxyListenAddress) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", s.cfg.ProxyListenAddress, err) + } + + // The factories back both surfaces over the same rage connection and + // discoverer: OCR endpoints and DON-to-DON peer groups. + s.grpcServer = grpc.NewServer() + creproxy.RegisterBinaryNetworkEndpointProxyServer(s.grpcServer, NewServer(factories.OCR2Endpoint, metrics)) + creproxy.RegisterEndpoint2ProxyServer(s.grpcServer, NewEndpoint2Server(factories.OCR3_1Endpoint, metrics)) + creproxy.RegisterPeerGroupProxyServer(s.grpcServer, NewPeerGroupServer(factories.PeerGroup, metrics)) + + // Gracefully stop the gRPC server when the engine cancels this context on + // Close; run the (blocking) Serve in a tracked goroutine so start returns + // promptly, per the services.Engine contract. + s.eng.Go(func(ctx context.Context) { + <-ctx.Done() + s.grpcServer.GracefulStop() + }) + s.eng.Go(func(context.Context) { + s.lggr.Infow("p2p proxy serving", "address", lis.Addr().String()) + if err := s.grpcServer.Serve(lis); err != nil { + s.eng.Errorw("proxy gRPC server stopped", "err", err) + } + }) + return nil +} + +// close tears down the libocr factories (local peer or proxy clients). The gRPC +// server is gracefully stopped by the goroutine started in start once the +// engine cancels its context. +func (s *proxyService) close() error { + if s.factoriesClose != nil { + return s.factoriesClose() + } + return nil +} diff --git a/crecore/server.go b/crecore/server.go new file mode 100644 index 000000000..efa864164 --- /dev/null +++ b/crecore/server.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "io" + "sync" + + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// Server implements the BinaryNetworkEndpointProxy gRPC service. It is backed +// by a real libocr BinaryNetworkEndpointFactory (i.e. a running rage peer) and +// exposes it over the network so that an out-of-process client can drive OCR +// message passing without owning the peer. +// +// Each Connect stream corresponds to exactly one BinaryNetworkEndpoint: the +// first message on the stream must be a NewEndpointRequest, after which the +// stream carries SendTo/Broadcast requests up and received messages down. +type Server struct { + creproxy.UnimplementedBinaryNetworkEndpointProxyServer + + peerFactory types.BinaryNetworkEndpointFactory + inboundSizes sizeRecorder + outboundSizes sizeRecorder +} + +// NewServer returns a Server that serves endpoints created by the given +// factory, typically networking.NewPeer(...).OCR2BinaryNetworkEndpointFactory(). +func NewServer(peerFactory types.BinaryNetworkEndpointFactory, metrics *proxyMetrics) *Server { + return &Server{ + peerFactory: peerFactory, + inboundSizes: metrics.sizes(endpointOCR2, directionInbound), + outboundSizes: metrics.sizes(endpointOCR2, directionOutbound), + } +} + +func (s *Server) Connect(stream creproxy.BinaryNetworkEndpointProxy_ConnectServer) error { + var closers []io.Closer + wg := sync.WaitGroup{} + + defer func() { + for _, c := range closers { + _ = c.Close() + } + wg.Wait() + }() + + req, err := stream.Recv() + if err != nil { + return fmt.Errorf("failed to receive initial NewEndpointRequest: %w", err) + } + + newEndpointReq, ok := req.Message.(*creproxy.BinaryNetworkClientRequest_NewEndpoint) + if !ok { + return fmt.Errorf("first message must be NewEndpointRequest, got %T", req.Message) + } + + endpoint, err := s.handleNewEndpoint(newEndpointReq.NewEndpoint) + if err != nil { + return fmt.Errorf("failed to create endpoint: %w", err) + } + closers = append(closers, endpoint) + + recvChan := endpoint.Receive() + + ctx := stream.Context() + wg.Add(1) + go func() { + defer wg.Done() + for msg := range recvChan { + pbMsg := &creproxy.BinaryMessageWithSender{ + Msg: msg.Msg, + Sender: uint32(msg.Sender), + } + s.inboundSizes.record(ctx, len(msg.Msg)) + if err := stream.Send(pbMsg); err != nil { + return + } + } + }() + + for { + req, err := stream.Recv() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + switch msg := req.Message.(type) { + case *creproxy.BinaryNetworkClientRequest_NewEndpoint: + return fmt.Errorf("NewEndpointRequest not allowed after initial setup") + case *creproxy.BinaryNetworkClientRequest_SendTo: + s.outboundSizes.record(ctx, len(msg.SendTo.Payload)) + endpoint.SendTo(msg.SendTo.Payload, commontypes.OracleID(msg.SendTo.ToOracleId)) + case *creproxy.BinaryNetworkClientRequest_Broadcast: + s.outboundSizes.record(ctx, len(msg.Broadcast)) + endpoint.Broadcast(msg.Broadcast) + } + } +} + +func (s *Server) handleNewEndpoint(req *creproxy.NewEndpointRequest) (commontypes.BinaryNetworkEndpoint, error) { + bootstrappers := make([]commontypes.BootstrapperLocator, len(req.V2Bootstrappers)) + for i, b := range req.V2Bootstrappers { + bootstrappers[i] = commontypes.BootstrapperLocator{ + PeerID: b.PeerId, + Addrs: b.Addrs, + } + } + + if len(req.ConfigDigest) != len(types.ConfigDigest{}) { + return nil, fmt.Errorf("invalid config digest length: got %d, expected %d", len(req.ConfigDigest), len(types.ConfigDigest{})) + } + var configDigest types.ConfigDigest + copy(configDigest[:], req.ConfigDigest) + + endpoint, err := s.peerFactory.NewEndpoint( + configDigest, + req.PeerIds, + bootstrappers, + int(req.FailureThreshold), + types.BinaryNetworkEndpointLimits{ + MaxMessageLength: int(req.Limits.MaxMessageLength), + MessagesRatePerOracle: req.Limits.MessagesRatePerOracle, + MessagesCapacityPerOracle: int(req.Limits.MessagesCapacityPerOracle), + BytesRatePerOracle: req.Limits.BytesRatePerOracle, + BytesCapacityPerOracle: int(req.Limits.BytesCapacityPerOracle), + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to create endpoint: %w", err) + } + + if err := endpoint.Start(); err != nil { + return nil, fmt.Errorf("failed to start endpoint: %w", err) + } + + return endpoint, nil +} From cb8a716a28327d22ec1918b531d31414cbc9e555 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 5 Aug 2026 13:55:32 -0400 Subject: [PATCH 03/30] Prepare libs for cap registry --- libs/go.mod | 179 ++++++----- libs/go.sum | 480 ++++++++++++++++++++---------- libs/standalone/evm/dependency.go | 188 ++++++++++++ libs/standalone/ocr/dependency.go | 8 + 4 files changed, 628 insertions(+), 227 deletions(-) create mode 100644 libs/standalone/evm/dependency.go diff --git a/libs/go.mod b/libs/go.mod index 635e5c90c..6fe4a90e6 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -12,18 +12,21 @@ require ( github.com/pressly/goose/v3 v3.27.1 github.com/prometheus/client_golang v1.23.2 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e - github.com/smartcontractkit/chainlink-common/keystore v1.2.0 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 + github.com/smartcontractkit/chainlink-common/keystore v1.3.0 + github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d - github.com/smartcontractkit/libocr v0.0.0-20260508200755-99940c85383c + github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.uber.org/zap v1.27.1 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 + go.uber.org/zap v1.28.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 ) @@ -43,32 +46,33 @@ require ( filippo.io/nistec v0.0.4 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect - github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/DataDog/zstd v1.5.7 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/NethermindEth/juno v0.12.5 // indirect - github.com/NethermindEth/starknet.go v0.8.0 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect - github.com/XSAM/otelsql v0.37.0 // indirect + github.com/NethermindEth/juno v0.15.11 // indirect + github.com/NethermindEth/starknet.go v0.17.1 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect + github.com/XSAM/otelsql v0.42.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect - github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect - github.com/buger/jsonparser v1.1.2 // indirect + github.com/buger/jsonparser v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect - github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect - github.com/cockroachdb/errors v1.11.3 // indirect - github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect - github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect + github.com/cockroachdb/errors v1.12.0 // indirect + github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect + github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/pebble v1.1.5 // indirect - github.com/cockroachdb/redact v1.1.5 // indirect - github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/cockroachdb/redact v1.1.6 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect github.com/cometbft/cometbft v0.38.21 // indirect github.com/cometbft/cometbft-db v0.14.1 // indirect - github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/consensys/gnark-crypto v0.20.1 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.1 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect @@ -77,26 +81,31 @@ require ( github.com/cosmos/gogoproto v1.7.0 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect - github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.6.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/deckarep/golang-set/v2 v2.9.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect - github.com/ethereum/go-ethereum v1.17.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/emicklei/dot v1.6.2 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect + github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect + github.com/ethereum/go-ethereum v1.17.3 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/ferranbt/fastssz v0.1.4 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/binary v0.8.0 // indirect github.com/gagliardetto/solana-go v1.13.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect - github.com/getsentry/sentry-go v0.27.0 // indirect - github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // 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.0 // indirect @@ -105,72 +114,93 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect - github.com/grafana/pyroscope-go v1.2.8 // indirect - github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grafana/pyroscope-go v1.3.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect + github.com/invopop/jsonschema v0.14.0 // indirect + github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgtype v1.14.4 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/linxGnu/grocksdb v1.8.14 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/mfridman/interpolate v0.0.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect + github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.2.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/stun/v2 v2.0.0 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/cors v1.11.1 // indirect github.com/rs/zerolog v1.33.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect @@ -179,27 +209,37 @@ require ( github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/chain-selectors v1.0.100 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect - github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect + github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 // indirect + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 // indirect + github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect - github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect + github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect github.com/stellar/go-stellar-sdk v0.4.0 // indirect github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/urfave/cli/v2 v2.27.7 // indirect + github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xssnick/tonutils-go v1.14.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zondax/hid v0.9.2 // indirect @@ -207,21 +247,21 @@ require ( go.dedis.ch/fixbuf v1.0.3 // indirect go.dedis.ch/kyber/v3 v3.1.0 // indirect go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect - go.mongodb.org/mongo-driver v1.12.2 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect @@ -230,6 +270,8 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect golang.org/x/mod v0.36.0 // indirect @@ -241,7 +283,10 @@ require ( golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + gopkg.in/guregu/null.v4 v4.0.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect pgregory.net/rapid v1.1.0 // indirect diff --git a/libs/go.sum b/libs/go.sum index 462cf05ba..096a902b7 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -32,31 +32,34 @@ github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwR github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR8jAwb1Ie9GyehWjVcGh32Y2MznE= -github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/NethermindEth/juno v0.12.5 h1:a+KYQg8MxzNJIbbqGHq+vU9nTyuWu3acbyXxcUPUDOY= -github.com/NethermindEth/juno v0.12.5/go.mod h1:XonWmZVRwCVHv1gjoVCoTFiZnYObwdukpd3NCsl04bA= -github.com/NethermindEth/starknet.go v0.8.0 h1:mGh7qDWrvuXJPcgGJP31DpifzP6+Ef2gt/BQhaqsV40= -github.com/NethermindEth/starknet.go v0.8.0/go.mod h1:slNA8PxtxA/0LQv0FwHnL3lHFDNhVZfTK6U2gjVb7l8= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/NethermindEth/juno v0.15.11 h1:v8nVO6ccvNx4eNmI6b6cKfGmRiucx0Y7QpgYJks6gz0= +github.com/NethermindEth/juno v0.15.11/go.mod h1:DyfDC1vz8OpoAOWdGJif97Kueo4J7yhZUtYkkFUYg20= +github.com/NethermindEth/starknet.go v0.17.1 h1:VmB81n2GX8m+bFisXVCF5Z6k+uHpDglyNkUCqTVqAJo= +github.com/NethermindEth/starknet.go v0.17.1/go.mod h1:72WzcIncBwvAUANawfRtKRR+6nUrc9eYMYs6QEbbh1Y= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 h1:/97whAzwYxMNHXeTfhAtCRzNCpyblmxCtSYpsfzCszM= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= -github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= +github.com/XSAM/otelsql v0.42.0 h1:Li0xF4eJUxG2e0x3D4rvRlys1f27yJKvjTh7ljkUP5o= +github.com/XSAM/otelsql v0.42.0/go.mod h1:4mOrEv+cS1KmKzrvTktvJnstr5GtKSAK+QHvFR9OcpI= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= +github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= -github.com/apache/arrow-go/v18 v18.3.1/go.mod h1:12QBya5JZT6PnBihi5NJTzbACrDGXYkrgjujz3MRQXU= +github.com/apache/arrow-go/v18 v18.6.0 h1:GX/Jyd3R7mCLiECAwY9FWbbaYblie2WXBSz4Sw8fNpM= +github.com/apache/arrow-go/v18 v18.6.0/go.mod h1:gm3MiPpY82fLYK5VKPB3WoJbsiLVDfT7flD5/vHReKw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -66,8 +69,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= @@ -76,8 +79,8 @@ github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/ github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= -github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= +github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= @@ -95,32 +98,35 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 h1:nLaJZcVAnaqch3K83AyzHfY2DmQM18/L7jvkmKSfkpI= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1/go.mod h1:6Q+F2puKpJ6zWv+R02BVnizJICf7++oRT5zwpZQAsbk= -github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= -github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCeN8rge3F/deT52w2WJMmIC5mHNUq+Ut8= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= +github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= +github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= -github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= -github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= -github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= +github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= -github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= -github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +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/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= -github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= -github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= +github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= @@ -142,10 +148,12 @@ github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5Rtn github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= @@ -155,12 +163,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= -github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= -github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI= +github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= @@ -186,15 +194,15 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= -github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= +github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= +github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= +github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= @@ -206,12 +214,12 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= @@ -220,21 +228,23 @@ github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdF github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= -github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= -github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= +github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -252,22 +262,23 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -302,20 +313,18 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -329,6 +338,7 @@ 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -336,26 +346,26 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= -github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= -github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8= +github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= @@ -407,14 +417,61 @@ github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSH github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= +github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 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= @@ -437,18 +494,19 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -458,31 +516,38 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= -github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -495,6 +560,7 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= @@ -504,13 +570,13 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -536,14 +602,16 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= @@ -552,17 +620,21 @@ github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -593,6 +665,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -600,12 +674,16 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -616,47 +694,59 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e h1:rg0UBvdzvZzDmWn2gURQt8+cnI7NZDhFA1dhTdZw6rQ= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= -github.com/smartcontractkit/chainlink-common/keystore v1.2.0 h1:1BH/b14CkGjArfzznlioQpIJiynECWVT48JUP9E277U= -github.com/smartcontractkit/chainlink-common/keystore v1.2.0/go.mod h1:9R/74vN+bJ5PbkOyM/pUy/AeAZaRwYb/k4XPeXcbDio= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 h1:XnMKhJDTl+JY+W7mIU1NJfaHqC4ykMXW0rz8hk9x5PQ= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= +github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= +github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 h1:0vivai99gZjV92RLlxnIhUBTODg08JfdBD8wLfN8A5A= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872/go.mod h1:xxdndutJLTdMDdJwazJDCmBh8H/d7nwc4aj7ZqVbzN4= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb h1:HQN56hEteWOBagVDCxV2Fn++frjRI7dfnozEQUq/5ok= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 h1:N1Q6NHuH+T2b6cst43LD2pnykNJSymP/szJJ7rrYY8I= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 h1:71PGTkjdFZ0JrloEC2Fs8eHl1b1gmUuH+bq7q23usKk= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d h1:DDoHNG+dMIixwGAQaFUORdreS/fcbw+AotZA1CcrJwY= github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d/go.mod h1:P38krjjMclE8tCnuBB5/RhC9+ouGaRL6XXRMdw1gVBE= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 h1:NJdGFhzT6zMaTod4QkBqVD2sg0I25iw1boOYtTpEwRo= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe/go.mod h1:z7lx7wI3XZ4u9kmUtAVdwn1BCC9T8aieWSDcuDgPTdQ= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 h1:/vkKPJoweLkRd56V4YHGRAtTG4+/JAlgklGEfvH6l4c= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 h1:LQy2j2+TdKLSWsUTUYuqmQPn8kjqCLjGI3ZJYGtDc08= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= -github.com/smartcontractkit/libocr v0.0.0-20260508200755-99940c85383c h1:meDKygNIR0tdT3Xmxe9NwyuiaCCDL0a9COqZ+4cL89g= -github.com/smartcontractkit/libocr v0.0.0-20260508200755-99940c85383c/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd h1:ksFjz3ytjK4kH5HFHpLKzDS0/9gmeSuvii1rs8FlxrI= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb h1:kLHdQQkijaPGsBbtV2rJgpzVpQ96e7T10pzjNlWfK8U= github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb/go.mod h1:4s5hj/nlMF9WV+T5Uhy4n9IYpRpzfJzT+vTKkNT7T+Y= github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= @@ -682,10 +772,11 @@ github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyi github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -696,13 +787,14 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= @@ -711,35 +803,42 @@ 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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg= github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= @@ -755,39 +854,39 @@ go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= -go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= -go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= @@ -807,32 +906,52 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -843,11 +962,13 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -860,6 +981,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -874,6 +996,12 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -887,6 +1015,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -895,11 +1024,15 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -928,24 +1061,39 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -954,8 +1102,14 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -963,15 +1117,16 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -984,10 +1139,10 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -1017,7 +1172,11 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -1039,6 +1198,7 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= diff --git a/libs/standalone/evm/dependency.go b/libs/standalone/evm/dependency.go new file mode 100644 index 000000000..afd5f91a7 --- /dev/null +++ b/libs/standalone/evm/dependency.go @@ -0,0 +1,188 @@ +// Package evm provides a standalone.BootstrapDependency that supplies an EVM RPC +// client to a standalone binary. +// +// The client is chainlink-evm's multinode-backed client.Client, not a bare geth +// *ethclient.Client. That is deliberate: multinode is where the RPC reliability +// behaviour lives (per-node health polling, sync-threshold detection, dead-node +// declaration, primary selection, load-balanced RPC support). A standalone process +// reading a contract needs exactly those properties and should not reimplement +// them. +// +// What this does not bring along is the relayer / ContractReader stack. Callers +// get a client.Client and make bind.ContractCaller view calls against generated +// gethwrappers directly, which is a far shorter path to follow than +// relayer -> ContractReader -> codec -> ReadIdentifier. +package evm + +import ( + "context" + "errors" + "fmt" + "math/big" + "time" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" + "github.com/smartcontractkit/chainlink-evm/pkg/config/chaintype" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +// Defaults mirror chainlink's own EVM chain defaults for a generic EVM chain, so +// a standalone binary polling the same RPCs behaves like the node does. +const ( + defaultSelectionMode = "HighestHead" + defaultLeaseDuration = 0 + defaultPollFailureThreshold = uint32(5) + defaultPollSuccessThreshold = uint32(0) + defaultPollInterval = 10 * time.Second + defaultSyncThreshold = uint32(5) + defaultNoNewHeadsThreshold = 3 * time.Minute + defaultFinalityDepth = uint32(50) + defaultFinalizedBlockOffset = uint32(0) + defaultDeathDeclarationDelay = 10 * time.Second + defaultNoNewFinalizedHeads = 0 + defaultFinalizedBlockPollInterval = 5 * time.Second + defaultNewHeadsPollInterval = 0 + defaultConfirmationTimeout = 60 * time.Second + defaultSafeDepth = uint32(0) +) + +// Dependency returns a standalone.BootstrapDependency that resolves a dialed, +// multinode-backed EVM client. +// +// At least one --evm-http-url is required. WebSocket URLs are optional; without +// them the client polls for heads rather than subscribing, which is enough for +// view calls. +func Dependency(lggr logger.Logger) standalone.BootstrapDependency[evmclient.Client] { + // Wrap in OnceBootstrapper so Get (which dials every configured RPC) runs at + // most once even if several services resolve this dependency. + return standalone.OnceBootstrapper[evmclient.Client](&dependency{lggr: lggr}) +} + +type dependency struct { + lggr logger.Logger + + client evmclient.Client + + httpURLs []string + wsURLs []string + chainID string + chainType string + finalityTagEnabled bool + finalityDepth uint32 + pollInterval time.Duration +} + +var _ standalone.BootstrapDependency[evmclient.Client] = (*dependency)(nil) + +func (d *dependency) AddCommands(cmd *cobra.Command) { + f := cmd.PersistentFlags() + f.StringSliceVar(&d.httpURLs, "evm-http-url", nil, "EVM RPC HTTP URL(s); repeat or comma-separate for a multinode pool") + f.StringSliceVar(&d.wsURLs, "evm-ws-url", nil, "EVM RPC WebSocket URL(s), positionally paired with --evm-http-url; optional") + f.StringVar(&d.chainID, "evm-chain-id", "", "EVM chain ID") + f.StringVar(&d.chainType, "evm-chain-type", "", "EVM chain type (empty for a generic EVM chain)") + f.BoolVar(&d.finalityTagEnabled, "evm-finality-tag-enabled", true, "use the finalized block tag instead of a finality depth") + f.Uint32Var(&d.finalityDepth, "evm-finality-depth", defaultFinalityDepth, "finality depth, used when --evm-finality-tag-enabled=false") + f.DurationVar(&d.pollInterval, "evm-poll-interval", defaultPollInterval, "per-node health poll interval") + + standalone.BindWithEnvVar(f.Lookup("evm-http-url")) + standalone.BindWithEnvVar(f.Lookup("evm-ws-url")) + standalone.BindWithEnvVar(f.Lookup("evm-chain-id")) +} + +func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmclient.Client, error) { + if len(d.httpURLs) == 0 { + return nil, errors.New("at least one --evm-http-url is required") + } + if len(d.wsURLs) > 0 && len(d.wsURLs) != len(d.httpURLs) { + return nil, fmt.Errorf("--evm-ws-url count (%d) must match --evm-http-url count (%d) when provided", len(d.wsURLs), len(d.httpURLs)) + } + + chainID, ok := new(big.Int).SetString(d.chainID, 10) + if !ok { + return nil, fmt.Errorf("invalid --evm-chain-id %q", d.chainID) + } + + nodeCfgs := make([]evmclient.NodeConfig, len(d.httpURLs)) + for i := range d.httpURLs { + name := fmt.Sprintf("node-%d", i) + order := int32(1) + sendOnly := false + loadBalanced := false + cfg := evmclient.NodeConfig{ + Name: &name, + HTTPURL: &d.httpURLs[i], + Order: &order, + SendOnly: &sendOnly, + IsLoadBalancedRPC: &loadBalanced, + } + if len(d.wsURLs) > 0 { + cfg.WSURL = &d.wsURLs[i] + } + nodeCfgs[i] = cfg + } + + selectionMode := defaultSelectionMode + pollFailureThreshold := defaultPollFailureThreshold + pollSuccessThreshold := defaultPollSuccessThreshold + syncThreshold := defaultSyncThreshold + nodeIsSyncingEnabled := false + finalityDepth := d.finalityDepth + finalityTagEnabled := d.finalityTagEnabled + safeTagSupported := false + finalizedBlockOffset := defaultFinalizedBlockOffset + enforceRepeatableRead := true + safeDepth := defaultSafeDepth + + chainCfg, nodePool, nodes, err := evmclient.NewClientConfigs( + &selectionMode, + defaultLeaseDuration, + d.chainType, + nodeCfgs, + &pollFailureThreshold, + &pollSuccessThreshold, + d.pollInterval, + &syncThreshold, + &nodeIsSyncingEnabled, + defaultNoNewHeadsThreshold, + &finalityDepth, + &finalityTagEnabled, + &safeTagSupported, + &finalizedBlockOffset, + &enforceRepeatableRead, + defaultDeathDeclarationDelay, + defaultNoNewFinalizedHeads, + defaultFinalizedBlockPollInterval, + defaultNewHeadsPollInterval, + defaultConfirmationTimeout, + &safeDepth, + ) + if err != nil { + return nil, fmt.Errorf("failed to build evm client configs: %w", err) + } + + // clientErrors is nil: it only feeds ClassifySendError on the transaction send + // path, and this client is read-only. + cl, err := evmclient.NewEvmClient(nodePool, chainCfg, nil, d.lggr, chainID, nodes, chaintype.ChainType(d.chainType)) + if err != nil { + return nil, fmt.Errorf("failed to create evm client: %w", err) + } + + if err := cl.Dial(ctx); err != nil { + return nil, fmt.Errorf("failed to dial evm client: %w", err) + } + + d.lggr.Infow("EVM client dialed", "chainID", chainID, "nodes", len(nodes), "selectionMode", selectionMode) + d.client = cl + return cl, nil +} + +func (d *dependency) Close() { + if d.client != nil { + d.client.Close() + } +} diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index 0e4fc7cb9..a6b906ec5 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -51,6 +51,12 @@ type Factories struct { // PeerGroup creates DON-to-DON peer groups. PeerGroup creproxy.PeerGroupFactory + // PeerID is the node's rage P2P identity, loaded from the keystore. Both + // modes resolve it, and consumers other than libocr need it: the on-chain + // CapabilitiesRegistry keys node records by peer ID, so anything reading + // registry metadata must know which node it is. + PeerID ragetypes.PeerID + closer io.Closer } @@ -173,6 +179,7 @@ func (d *dependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, OCR2Endpoint: peer.OCR2BinaryNetworkEndpointFactory(), OCR3_1Endpoint: peer.OCR3_1BinaryNetworkEndpointFactory(), PeerGroup: newNetworkingPeerGroupFactory(peer.PeerGroupFactory()), + PeerID: peerID, closer: peer, }, nil } @@ -204,6 +211,7 @@ func (d *dependency) proxyFactories(peerID ragetypes.PeerID) (*Factories, error) OCR2Endpoint: endpointFactory, OCR3_1Endpoint: endpoint2Factory, PeerGroup: pgFactory, + PeerID: peerID, closer: multiCloser{endpointFactory, endpoint2Factory, pgFactory}, }, nil } From 37af89249daf2fe31194b0d55d818497aae9a0c6 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 5 Aug 2026 14:04:43 -0400 Subject: [PATCH 04/30] Delegate capabilty registry --- crecore/go.mod | 157 +++++--- crecore/go.sum | 459 ++++++++++++++++-------- crecore/main.go | 42 ++- crecore/proxy_service.go | 13 +- crecore/registry/local_registry.go | 244 +++++++++++++ crecore/registry/local_registry_test.go | 218 +++++++++++ crecore/registry/metadata.go | 62 ++++ crecore/registry/syncer.go | 298 +++++++++++++++ crecore/registry/syncer_test.go | 309 ++++++++++++++++ crecore/registry_service.go | 137 +++++++ 10 files changed, 1725 insertions(+), 214 deletions(-) create mode 100644 crecore/registry/local_registry.go create mode 100644 crecore/registry/local_registry_test.go create mode 100644 crecore/registry/metadata.go create mode 100644 crecore/registry/syncer.go create mode 100644 crecore/registry/syncer_test.go create mode 100644 crecore/registry_service.go diff --git a/crecore/go.mod b/crecore/go.mod index aa96f5f1b..27e60817e 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -3,11 +3,15 @@ module github.com/smartcontractkit/capabilities/crecore go 1.26.2 require ( - github.com/smartcontractkit/capabilities/libs v0.0.0-20260730175407-a6d1af3f36c8 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e + github.com/ethereum/go-ethereum v1.17.3 + github.com/smartcontractkit/capabilities/libs v0.0.0-20260805175532-500ba79bfb4d + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 + github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 @@ -30,23 +34,24 @@ require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect github.com/DataDog/zstd v1.5.7 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/NethermindEth/juno v0.12.5 // indirect - github.com/NethermindEth/starknet.go v0.8.0 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect - github.com/XSAM/otelsql v0.37.0 // indirect + github.com/NethermindEth/juno v0.15.11 // indirect + github.com/NethermindEth/starknet.go v0.17.1 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect + github.com/XSAM/otelsql v0.42.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect - github.com/bits-and-blooms/bitset v1.24.0 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect - github.com/buger/jsonparser v1.1.2 // indirect + github.com/buger/jsonparser v1.2.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect - github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect @@ -55,7 +60,7 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect github.com/cometbft/cometbft v0.38.21 // indirect github.com/cometbft/cometbft-db v0.14.1 // indirect - github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/consensys/gnark-crypto v0.20.1 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.1 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect @@ -64,26 +69,30 @@ require ( github.com/cosmos/gogoproto v1.7.0 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect - github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.8.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/deckarep/golang-set/v2 v2.9.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect - github.com/ethereum/go-ethereum v1.17.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/emicklei/dot v1.6.2 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect + github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/ferranbt/fastssz v0.1.4 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/binary v0.8.0 // indirect github.com/gagliardetto/solana-go v1.13.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/getsentry/sentry-go v0.35.1 // indirect - github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // 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.0 // indirect @@ -92,27 +101,30 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect - github.com/grafana/pyroscope-go v1.2.8 // indirect - github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grafana/pyroscope-go v1.3.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.3 // indirect @@ -120,42 +132,57 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect + github.com/invopop/jsonschema v0.14.0 // indirect + github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgtype v1.14.4 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/linxGnu/grocksdb v1.8.14 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/mfridman/interpolate v0.0.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect + github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.2.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/stun/v2 v2.0.0 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pressly/goose/v3 v3.27.1 // indirect @@ -164,7 +191,9 @@ require ( github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/cors v1.11.1 // indirect github.com/rs/zerolog v1.33.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect @@ -174,13 +203,16 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/chain-selectors v1.0.100 // indirect - github.com/smartcontractkit/chainlink-common/keystore v1.2.0 // indirect + github.com/smartcontractkit/chainlink-common/keystore v1.3.0 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect + github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 // indirect + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b // indirect - github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect + github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect - github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect + github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect @@ -192,15 +224,20 @@ require ( github.com/stellar/go-stellar-sdk v0.4.0 // indirect github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/urfave/cli/v2 v2.27.7 // indirect + github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xssnick/tonutils-go v1.14.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zondax/hid v0.9.2 // indirect @@ -208,21 +245,21 @@ require ( go.dedis.ch/fixbuf v1.0.3 // indirect go.dedis.ch/kyber/v3 v3.1.0 // indirect go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect - go.mongodb.org/mongo-driver v1.12.2 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect @@ -230,9 +267,10 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect golang.org/x/net v0.55.0 // indirect @@ -242,9 +280,12 @@ require ( golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/guregu/null.v4 v4.0.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect pgregory.net/rapid v1.1.0 // indirect diff --git a/crecore/go.sum b/crecore/go.sum index 81c66b293..9d54954a8 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -34,29 +34,32 @@ github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dX github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/NethermindEth/juno v0.12.5 h1:a+KYQg8MxzNJIbbqGHq+vU9nTyuWu3acbyXxcUPUDOY= -github.com/NethermindEth/juno v0.12.5/go.mod h1:XonWmZVRwCVHv1gjoVCoTFiZnYObwdukpd3NCsl04bA= -github.com/NethermindEth/starknet.go v0.8.0 h1:mGh7qDWrvuXJPcgGJP31DpifzP6+Ef2gt/BQhaqsV40= -github.com/NethermindEth/starknet.go v0.8.0/go.mod h1:slNA8PxtxA/0LQv0FwHnL3lHFDNhVZfTK6U2gjVb7l8= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/NethermindEth/juno v0.15.11 h1:v8nVO6ccvNx4eNmI6b6cKfGmRiucx0Y7QpgYJks6gz0= +github.com/NethermindEth/juno v0.15.11/go.mod h1:DyfDC1vz8OpoAOWdGJif97Kueo4J7yhZUtYkkFUYg20= +github.com/NethermindEth/starknet.go v0.17.1 h1:VmB81n2GX8m+bFisXVCF5Z6k+uHpDglyNkUCqTVqAJo= +github.com/NethermindEth/starknet.go v0.17.1/go.mod h1:72WzcIncBwvAUANawfRtKRR+6nUrc9eYMYs6QEbbh1Y= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 h1:/97whAzwYxMNHXeTfhAtCRzNCpyblmxCtSYpsfzCszM= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= -github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= +github.com/XSAM/otelsql v0.42.0 h1:Li0xF4eJUxG2e0x3D4rvRlys1f27yJKvjTh7ljkUP5o= +github.com/XSAM/otelsql v0.42.0/go.mod h1:4mOrEv+cS1KmKzrvTktvJnstr5GtKSAK+QHvFR9OcpI= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= +github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= -github.com/apache/arrow-go/v18 v18.3.1/go.mod h1:12QBya5JZT6PnBihi5NJTzbACrDGXYkrgjujz3MRQXU= +github.com/apache/arrow-go/v18 v18.6.0 h1:GX/Jyd3R7mCLiECAwY9FWbbaYblie2WXBSz4Sw8fNpM= +github.com/apache/arrow-go/v18 v18.6.0/go.mod h1:gm3MiPpY82fLYK5VKPB3WoJbsiLVDfT7flD5/vHReKw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -66,18 +69,19 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= -github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 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/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= -github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= +github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= @@ -95,12 +99,13 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 h1:nLaJZcVAnaqch3K83AyzHfY2DmQM18/L7jvkmKSfkpI= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1/go.mod h1:6Q+F2puKpJ6zWv+R02BVnizJICf7++oRT5zwpZQAsbk= -github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= -github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCeN8rge3F/deT52w2WJMmIC5mHNUq+Ut8= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= +github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= +github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= @@ -119,8 +124,10 @@ github.com/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzg github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= -github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= -github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= +github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= @@ -142,10 +149,12 @@ github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5Rtn github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= @@ -155,12 +164,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= -github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI= +github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= @@ -184,15 +193,15 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= -github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= +github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= +github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= +github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= @@ -204,12 +213,12 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= @@ -223,16 +232,18 @@ github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvN github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -250,22 +261,23 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -300,20 +312,18 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -327,6 +337,7 @@ 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -334,26 +345,26 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= -github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= -github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8= +github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= @@ -405,14 +416,61 @@ github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSH github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= +github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 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= @@ -435,18 +493,19 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -456,31 +515,38 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= -github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -493,6 +559,7 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= @@ -502,13 +569,13 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -534,14 +601,16 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= @@ -550,17 +619,21 @@ github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -591,6 +664,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -598,12 +673,16 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -614,41 +693,55 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260730175407-a6d1af3f36c8 h1:KhwjNOJdP4v2dQxge/d85KH+Kry0klBtbNEOj2EjkUk= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260730175407-a6d1af3f36c8/go.mod h1:hGshOtcToWcu2Fai1p6mg+/ynKE3pKFHHRfoSiYLs7I= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260805175532-500ba79bfb4d h1:SAGzQVHlpxRZrTd7znT4keD8RwvrL3dM0Tv2GHtuIyA= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260805175532-500ba79bfb4d/go.mod h1:hZFz3d31VSBzN8n7eGdSjRoge4Wxgp59Swj1Cf7Mc90= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e h1:rg0UBvdzvZzDmWn2gURQt8+cnI7NZDhFA1dhTdZw6rQ= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260730160930-5e81e731d28e/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= -github.com/smartcontractkit/chainlink-common/keystore v1.2.0 h1:1BH/b14CkGjArfzznlioQpIJiynECWVT48JUP9E277U= -github.com/smartcontractkit/chainlink-common/keystore v1.2.0/go.mod h1:9R/74vN+bJ5PbkOyM/pUy/AeAZaRwYb/k4XPeXcbDio= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 h1:XnMKhJDTl+JY+W7mIU1NJfaHqC4ykMXW0rz8hk9x5PQ= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= +github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= +github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 h1:0vivai99gZjV92RLlxnIhUBTODg08JfdBD8wLfN8A5A= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872/go.mod h1:xxdndutJLTdMDdJwazJDCmBh8H/d7nwc4aj7ZqVbzN4= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 h1:JFo7C3FilwhfwGBLAyj2umbL+P4QxGmVi/b8yt9kqvI= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6/go.mod h1:a260YnLyWq2NHLUN5cSVyMGk9nhO6RguCaTI2rsVqyA= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb h1:HQN56hEteWOBagVDCxV2Fn++frjRI7dfnozEQUq/5ok= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 h1:N1Q6NHuH+T2b6cst43LD2pnykNJSymP/szJJ7rrYY8I= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 h1:71PGTkjdFZ0JrloEC2Fs8eHl1b1gmUuH+bq7q23usKk= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d h1:DDoHNG+dMIixwGAQaFUORdreS/fcbw+AotZA1CcrJwY= github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d/go.mod h1:P38krjjMclE8tCnuBB5/RhC9+ouGaRL6XXRMdw1gVBE= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 h1:NJdGFhzT6zMaTod4QkBqVD2sg0I25iw1boOYtTpEwRo= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe/go.mod h1:z7lx7wI3XZ4u9kmUtAVdwn1BCC9T8aieWSDcuDgPTdQ= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 h1:/vkKPJoweLkRd56V4YHGRAtTG4+/JAlgklGEfvH6l4c= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd h1:ksFjz3ytjK4kH5HFHpLKzDS0/9gmeSuvii1rs8FlxrI= @@ -678,10 +771,11 @@ github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyi github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -692,13 +786,14 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= @@ -707,35 +802,42 @@ 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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg= github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= @@ -751,39 +853,39 @@ go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= -go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= -go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= @@ -803,32 +905,52 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -839,13 +961,13 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -856,6 +978,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -870,6 +993,12 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -883,6 +1012,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -891,11 +1021,15 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -924,24 +1058,39 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -950,8 +1099,14 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -959,15 +1114,14 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -980,10 +1134,10 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -1013,7 +1167,11 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -1035,6 +1193,7 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= diff --git a/crecore/main.go b/crecore/main.go index dac8799d4..927b3d779 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -4,14 +4,19 @@ import ( "context" "embed" "log" + "time" "github.com/spf13/cobra" "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/db" + "github.com/smartcontractkit/capabilities/libs/standalone/evm" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/capabilities/crecore/registry" + "github.com/smartcontractkit/chainlink-common/pkg/services" + evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" ) //go:embed migrations/*.sql @@ -32,6 +37,11 @@ func main() { func run() error { cfg := &Config{} + var ( + registryAddress string + registrySyncInterval time.Duration + ) + root := &cobra.Command{ Use: "main", Short: "P2P proxy for the CRE", @@ -41,10 +51,21 @@ this process. The peer's identity is loaded from the node's keystore (shared DB via CL_DATABASE_URL, decrypted with CL_PASSWORD_KEYSTORE). Provide exactly one networking mode: --listen-addresses to run a local libocr -peer, or --proxy-address to delegate to another proxy.`, +peer, or --proxy-address to delegate to another proxy. + +It also serves the CapabilitiesRegistry on that same gRPC address, read directly +from chain with an EVM client (no relayer). Core uses that in place of its own +registrysyncer whenever it is delegating rage networking to this process, so +--capabilities-registry-address is required: this process running is what enables +the registry, and core does not start without it.`, } root.PersistentFlags().StringVar(&cfg.ProxyListenAddress, "proxy-listen-address", ":50051", "address the proxy gRPC server listens on") + root.PersistentFlags().StringVar(®istryAddress, "capabilities-registry-address", "", "on-chain CapabilitiesRegistry (v2) contract address (required)") + root.PersistentFlags().DurationVar(®istrySyncInterval, "capabilities-registry-sync-interval", registry.DefaultSyncInterval, "how often the on-chain registry is re-read") + if err := root.MarkPersistentFlagRequired("capabilities-registry-address"); err != nil { + return err + } bootstrapper := standalone.NewBootstrapper(root, standalone.WithOtelViews(metricViews())) lggr := bootstrapper.Logger() @@ -54,8 +75,21 @@ peer, or --proxy-address to delegate to another proxy.`, // OCR discoverer table. dbDep := db.Dependency(embeddedMigrations, migrationsTable) ocrDep := ocr.Dependency(lggr.Named("OCR"), dbDep, ocrDiscovererTable) + // The registry always runs, so the EVM client is always resolved and the + // --evm-* flags are as required in practice as the registry address is. + evmDep := evm.Dependency(lggr.Named("EVM")) - return standalone.Run1(bootstrapper, func(ctx context.Context, scfg *standalone.StandaloneConfig, factories standalone.Dependency[*ocr.Factories]) []services.Service { - return []services.Service{newProxyService(cfg, scfg.Logger.Named("proxy service"), factories)} - }, ocrDep) + return standalone.Run2(bootstrapper, func( + ctx context.Context, + scfg *standalone.StandaloneConfig, + factories standalone.Dependency[*ocr.Factories], + evmClient standalone.Dependency[evmclient.Client], + ) []services.Service { + regSvc := newRegistryService(registryAddress, registrySyncInterval, + scfg.Logger.Named("capabilities registry"), evmClient, factories) + // The registry attaches to the proxy's gRPC server, so core reaches both + // over the single --proxy-listen-address it already configures. + proxySvc := newProxyService(cfg, scfg.Logger.Named("proxy service"), factories, regSvc.Register) + return []services.Service{proxySvc, regSvc} + }, ocrDep, evmDep) } diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index c3c876553..08151d23a 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -39,6 +39,11 @@ type proxyService struct { lggr logger.Logger factories standalone.Dependency[*ocr.Factories] + // registrars attach additional gRPC services to this server before it + // Serves. Used so co-located services (e.g. the CapabilitiesRegistry) share + // one address instead of each opening a listener. + registrars []func(*grpc.Server) + grpcServer *grpc.Server factoriesClose func() error } @@ -48,8 +53,8 @@ var _ services.Service = (*proxyService)(nil) // newProxyService builds the proxy service using the standard // services.Config/Engine pattern, so its lifecycle and health integrate with // the bootstrapper's aggregated health report. -func newProxyService(cfg *Config, lggr logger.Logger, factories standalone.Dependency[*ocr.Factories]) *proxyService { - s := &proxyService{cfg: cfg, lggr: lggr, factories: factories} +func newProxyService(cfg *Config, lggr logger.Logger, factories standalone.Dependency[*ocr.Factories], registrars ...func(*grpc.Server)) *proxyService { + s := &proxyService{cfg: cfg, lggr: lggr, factories: factories, registrars: registrars} s.Service, s.eng = services.Config{ Name: "P2PProxy", Start: s.start, @@ -86,6 +91,10 @@ func (s *proxyService) start(ctx context.Context) error { creproxy.RegisterEndpoint2ProxyServer(s.grpcServer, NewEndpoint2Server(factories.OCR3_1Endpoint, metrics)) creproxy.RegisterPeerGroupProxyServer(s.grpcServer, NewPeerGroupServer(factories.PeerGroup, metrics)) + for _, register := range s.registrars { + register(s.grpcServer) + } + // Gracefully stop the gRPC server when the engine cancels this context on // Close; run the (blocking) Serve in a tracked goroutine so start returns // promptly, per the services.Engine contract. diff --git a/crecore/registry/local_registry.go b/crecore/registry/local_registry.go new file mode 100644 index 000000000..3a438a034 --- /dev/null +++ b/crecore/registry/local_registry.go @@ -0,0 +1,244 @@ +// Package registry implements the CapabilitiesRegistry that crecore serves over +// plain gRPC (see libs/creregistry/pb). +// +// The on-chain view is read with an EVM client and generated gethwrappers +// directly. There is no relayer, no ContractReader, no codec and no +// ReadIdentifier indirection: three view calls, decoded by the wrapper. +package registry + +import ( + "context" + "errors" + "fmt" + "sync" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// DonID is a DON's registry ID. +type DonID uint32 + +// DON pairs a DON's identity with the raw capability configuration blobs the +// contract stores for it. +type DON struct { + capabilities.DON + // CapabilityConfigurations maps capability ID to the wire-encoded + // capabilities/pb.CapabilityConfig the contract holds for this DON. + // + // The bytes are deliberately left undecoded. crecore has no reason to + // interpret them: it serves them verbatim over ConfigForCapability and the + // caller unmarshals. That keeps ~150 lines of config-decoding logic (and its + // drift risk) out of this process entirely. + CapabilityConfigurations map[string][]byte +} + +// NodeInfo is a node's on-chain record. +type NodeInfo struct { + NodeOperatorID uint32 + ConfigCount uint32 + WorkflowDONID uint32 + Signer [32]byte + P2pID [32]byte + EncryptionPublicKey [32]byte + CsaKey [32]byte + CapabilityIDs []string +} + +// Capability is a capability's on-chain record. +type Capability struct { + ID string + CapabilityType capabilities.CapabilityType +} + +// LocalRegistry is an immutable snapshot of the on-chain registry, plus the +// lookups the metadata API needs. It is ported from chainlink's +// core/services/registrysyncer.LocalRegistry, minus the ORM/DB persistence and +// with capability config left as raw bytes. +// +// A LocalRegistry is never mutated after construction; Syncer swaps whole +// snapshots. +type LocalRegistry struct { + Logger logger.Logger + GetPeerID func() (ragetypes.PeerID, error) + IDsToDONs map[DonID]DON + IDsToNodes map[ragetypes.PeerID]NodeInfo + IDsToCapabilities map[string]Capability + + cacheMu sync.RWMutex + cachedLocalNodePeer ragetypes.PeerID + cachedLocalNode capabilities.Node +} + +func NewLocalRegistry( + lggr logger.Logger, + getPeerID func() (ragetypes.PeerID, error), + idsToDONs map[DonID]DON, + idsToNodes map[ragetypes.PeerID]NodeInfo, + idsToCapabilities map[string]Capability, +) *LocalRegistry { + return &LocalRegistry{ + Logger: logger.Named(lggr, "LocalRegistry"), + GetPeerID: getPeerID, + IDsToDONs: idsToDONs, + IDsToNodes: idsToNodes, + IDsToCapabilities: idsToCapabilities, + } +} + +// LocalNode resolves this process's own node record. The result is cached: the +// peer ID cannot change at runtime, so a changed peer ID is logged as a bug +// rather than silently accepted. +func (l *LocalRegistry) LocalNode(ctx context.Context) (capabilities.Node, error) { + pid, err := l.GetPeerID() + if err != nil { + return capabilities.Node{}, fmt.Errorf("unable to get local node peer ID: %w", err) + } + + l.cacheMu.RLock() + if l.cachedLocalNodePeer != (ragetypes.PeerID{}) && l.cachedLocalNodePeer == pid { + node := l.cachedLocalNode + l.cacheMu.RUnlock() + return node, nil + } + l.cacheMu.RUnlock() + + l.cacheMu.Lock() + defer l.cacheMu.Unlock() + if l.cachedLocalNodePeer != (ragetypes.PeerID{}) && l.cachedLocalNodePeer == pid { + return l.cachedLocalNode, nil + } + + if l.cachedLocalNodePeer != (ragetypes.PeerID{}) { + l.Logger.Errorw("node's peerID changed at runtime, this should never happen", + "cachedLocalNodePeer", l.cachedLocalNodePeer, "currentPeerID", pid) + } + + n, err := l.NodeByPeerID(ctx, pid) + if err != nil { + return n, err + } + l.cachedLocalNode = n + l.cachedLocalNodePeer = pid + return n, nil +} + +func (l *LocalRegistry) NodeByPeerID(_ context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { + if err := l.ensureNotEmpty(); err != nil { + return capabilities.Node{}, err + } + nodeInfo, ok := l.IDsToNodes[peerID] + if !ok { + return capabilities.Node{}, errors.New("could not find peerID " + peerID.String()) + } + + var workflowDON capabilities.DON + var capabilityDONs []capabilities.DON + for _, d := range l.IDsToDONs { + for _, p := range d.Members { + if p != peerID { + continue + } + if d.AcceptsWorkflows { + // The CapabilitiesRegistry enforces DON ID > 0, so a zero ID here + // means workflowDON has not been assigned yet. + if workflowDON.ID == 0 { + workflowDON = d.DON + } else { + l.Logger.Errorw("Configuration error: node belongs to more than one workflowDON", "peerID", peerID) + } + } + capabilityDONs = append(capabilityDONs, d.DON) + } + } + + return capabilities.Node{ + PeerID: &peerID, + NodeOperatorID: nodeInfo.NodeOperatorID, + Signer: nodeInfo.Signer, + EncryptionPublicKey: nodeInfo.EncryptionPublicKey, + WorkflowDON: workflowDON, + CapabilityDONs: capabilityDONs, + }, nil +} + +func (l *LocalRegistry) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { + if err := l.ensureNotEmpty(); err != nil { + return nil, err + } + + found := []capabilities.DONWithNodes{} + for _, don := range l.IDsToDONs { + if _, ok := don.CapabilityConfigurations[capabilityID]; !ok { + continue + } + nodes, err := l.nodesForDON(ctx, don.DON) + if err != nil { + return nil, fmt.Errorf("could not fetch nodes for DON %d: %w", don.ID, err) + } + found = append(found, capabilities.DONWithNodes{DON: don.DON, Nodes: nodes}) + } + + if len(found) == 0 { + return nil, fmt.Errorf("could not find DON for capability %s", capabilityID) + } + return found, nil +} + +func (l *LocalRegistry) nodesForDON(ctx context.Context, don capabilities.DON) ([]capabilities.Node, error) { + nodes := make([]capabilities.Node, 0, len(don.Members)) + for _, n := range don.Members { + node, err := l.NodeByPeerID(ctx, n) + if err != nil { + return nil, fmt.Errorf("could not find node for peerID %s: %w", n.String(), err) + } + nodes = append(nodes, node) + } + return nodes, nil +} + +// DONByID resolves any DON known to the registry, including caller/workflow DONs +// that host no capability. Callers need this to read a caller DON's Families +// (e.g. zone membership) from its WorkflowDonID. +func (l *LocalRegistry) DONByID(_ context.Context, donID uint32) (capabilities.DON, error) { + if err := l.ensureNotEmpty(); err != nil { + return capabilities.DON{}, err + } + d, ok := l.IDsToDONs[DonID(donID)] + if !ok { + return capabilities.DON{}, fmt.Errorf("could not find don %d", donID) + } + return d.DON, nil +} + +// RawConfigForCapability returns the wire-encoded capabilities/pb.CapabilityConfig +// for a capability on a DON, undecoded. See DON.CapabilityConfigurations. +func (l *LocalRegistry) RawConfigForCapability(_ context.Context, capabilityID string, donID uint32) ([]byte, error) { + if err := l.ensureNotEmpty(); err != nil { + return nil, err + } + d, ok := l.IDsToDONs[DonID(donID)] + if !ok { + return nil, fmt.Errorf("could not find don %d", donID) + } + cfg, ok := d.CapabilityConfigurations[capabilityID] + if !ok { + return nil, fmt.Errorf("could not find capability configuration for capability %s and donID %d", capabilityID, donID) + } + return cfg, nil +} + +func (l *LocalRegistry) ensureNotEmpty() error { + if len(l.IDsToDONs) == 0 { + return errors.New("empty local registry: no DONs registered") + } + if len(l.IDsToNodes) == 0 { + return errors.New("empty local registry: no nodes registered") + } + if len(l.IDsToCapabilities) == 0 { + return errors.New("empty local registry: no capabilities registered") + } + return nil +} diff --git a/crecore/registry/local_registry_test.go b/crecore/registry/local_registry_test.go new file mode 100644 index 000000000..c9d971efb --- /dev/null +++ b/crecore/registry/local_registry_test.go @@ -0,0 +1,218 @@ +package registry + +import ( + "context" + "errors" + "testing" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +func peer(b byte) ragetypes.PeerID { + var p ragetypes.PeerID + p[0] = b + return p +} + +// testLocalRegistry builds a snapshot with: +// - DON 1: workflow DON (accepts workflows), members p1, p2 +// - DON 2: capability DON hosting act@1.0.0, members p1, p3 +func testLocalRegistry(t *testing.T, self ragetypes.PeerID) *LocalRegistry { + t.Helper() + + p1, p2, p3 := peer(1), peer(2), peer(3) + + return NewLocalRegistry( + logger.Test(t), + func() (ragetypes.PeerID, error) { return self, nil }, + map[DonID]DON{ + 1: { + DON: capabilities.DON{ + ID: 1, Name: "wf-don", F: 1, ConfigVersion: 3, + Members: []ragetypes.PeerID{p1, p2}, + Families: []string{"zone-a"}, + AcceptsWorkflows: true, + }, + CapabilityConfigurations: map[string][]byte{}, + }, + 2: { + DON: capabilities.DON{ + ID: 2, Name: "cap-don", F: 2, ConfigVersion: 5, + Members: []ragetypes.PeerID{p1, p3}, + }, + CapabilityConfigurations: map[string][]byte{"act@1.0.0": []byte("cfg-bytes")}, + }, + }, + map[ragetypes.PeerID]NodeInfo{ + p1: {NodeOperatorID: 11, P2pID: p1, Signer: [32]byte{0xaa}, WorkflowDONID: 1}, + p2: {NodeOperatorID: 12, P2pID: p2, WorkflowDONID: 1}, + p3: {NodeOperatorID: 13, P2pID: p3}, + }, + map[string]Capability{ + "act@1.0.0": {ID: "act@1.0.0", CapabilityType: capabilities.CapabilityTypeAction}, + }, + ) +} + +func TestLocalRegistry_NodeByPeerIDSplitsWorkflowAndCapabilityDONs(t *testing.T) { + ctx := context.Background() + lr := testLocalRegistry(t, peer(1)) + + // p1 is in both DONs: the workflow DON is the one that accepts workflows, + // and every DON it belongs to shows up under CapabilityDONs. + got, err := lr.NodeByPeerID(ctx, peer(1)) + require.NoError(t, err) + assert.Equal(t, uint32(11), got.NodeOperatorID) + assert.Equal(t, [32]byte{0xaa}, got.Signer) + assert.Equal(t, uint32(1), got.WorkflowDON.ID) + assert.Len(t, got.CapabilityDONs, 2) + + // p3 is only in the capability DON, so it has no workflow DON at all. + got3, err := lr.NodeByPeerID(ctx, peer(3)) + require.NoError(t, err) + assert.Zero(t, got3.WorkflowDON.ID) + require.Len(t, got3.CapabilityDONs, 1) + assert.Equal(t, uint32(2), got3.CapabilityDONs[0].ID) +} + +func TestLocalRegistry_NodeByPeerIDUnknownPeer(t *testing.T) { + ctx := context.Background() + lr := testLocalRegistry(t, peer(1)) + + _, err := lr.NodeByPeerID(ctx, peer(99)) + require.Error(t, err) +} + +func TestLocalRegistry_LocalNodeUsesGetPeerID(t *testing.T) { + ctx := context.Background() + + lr := testLocalRegistry(t, peer(3)) + got, err := lr.LocalNode(ctx) + require.NoError(t, err) + require.NotNil(t, got.PeerID) + assert.Equal(t, peer(3), *got.PeerID) + assert.Equal(t, uint32(13), got.NodeOperatorID) +} + +func TestLocalRegistry_LocalNodeCaches(t *testing.T) { + ctx := context.Background() + + calls := 0 + lr := testLocalRegistry(t, peer(1)) + lr.GetPeerID = func() (ragetypes.PeerID, error) { + calls++ + return peer(1), nil + } + + for i := 0; i < 3; i++ { + _, err := lr.LocalNode(ctx) + require.NoError(t, err) + } + + // GetPeerID is consulted every time (the cache is keyed on it), but the + // derived node is only built once. The cache is per-snapshot, so a new sync + // still yields fresh DON data. + assert.Equal(t, 3, calls) +} + +func TestLocalRegistry_LocalNodePeerIDError(t *testing.T) { + ctx := context.Background() + + lr := testLocalRegistry(t, peer(1)) + lr.GetPeerID = func() (ragetypes.PeerID, error) { return ragetypes.PeerID{}, errors.New("keystore locked") } + + _, err := lr.LocalNode(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "keystore locked") +} + +func TestLocalRegistry_DONsForCapability(t *testing.T) { + ctx := context.Background() + lr := testLocalRegistry(t, peer(1)) + + got, err := lr.DONsForCapability(ctx, "act@1.0.0") + require.NoError(t, err) + require.Len(t, got, 1) + assert.Equal(t, uint32(2), got[0].DON.ID) + + // Nodes must be populated, not left empty: callers read signers off them. + require.Len(t, got[0].Nodes, 2) + ops := []uint32{got[0].Nodes[0].NodeOperatorID, got[0].Nodes[1].NodeOperatorID} + assert.ElementsMatch(t, []uint32{11, 13}, ops) +} + +func TestLocalRegistry_DONsForCapabilityUnknown(t *testing.T) { + ctx := context.Background() + lr := testLocalRegistry(t, peer(1)) + + _, err := lr.DONsForCapability(ctx, "nope@1.0.0") + require.Error(t, err) +} + +func TestLocalRegistry_DONByIDResolvesWorkflowDON(t *testing.T) { + ctx := context.Background() + lr := testLocalRegistry(t, peer(1)) + + // DON 1 hosts no capability, so DONsForCapability would never surface it. + // DONByID must still resolve it, because callers read a caller DON's Families + // (zone membership) from its ID. + got, err := lr.DONByID(ctx, 1) + require.NoError(t, err) + assert.Equal(t, "wf-don", got.Name) + assert.Equal(t, []string{"zone-a"}, got.Families) + + _, err = lr.DONByID(ctx, 404) + require.Error(t, err) +} + +func TestLocalRegistry_RawConfigForCapability(t *testing.T) { + ctx := context.Background() + lr := testLocalRegistry(t, peer(1)) + + got, err := lr.RawConfigForCapability(ctx, "act@1.0.0", 2) + require.NoError(t, err) + assert.Equal(t, []byte("cfg-bytes"), got) + + // Wrong DON, and unknown capability on a known DON, are both errors. + _, err = lr.RawConfigForCapability(ctx, "act@1.0.0", 1) + require.Error(t, err) + + _, err = lr.RawConfigForCapability(ctx, "nope@1.0.0", 2) + require.Error(t, err) + + _, err = lr.RawConfigForCapability(ctx, "act@1.0.0", 404) + require.Error(t, err) +} + +func TestLocalRegistry_EmptySnapshotIsRejected(t *testing.T) { + ctx := context.Background() + + // An empty snapshot means the sync has not produced usable data. Answering + // from it would look like "this node belongs to no DON", which reads as a + // deliberate configuration rather than a missing read. + lr := NewLocalRegistry( + logger.Test(t), + func() (ragetypes.PeerID, error) { return peer(1), nil }, + map[DonID]DON{}, + map[ragetypes.PeerID]NodeInfo{}, + map[string]Capability{}, + ) + + _, err := lr.NodeByPeerID(ctx, peer(1)) + require.Error(t, err) + assert.Contains(t, err.Error(), "empty local registry") + + _, err = lr.DONByID(ctx, 1) + require.Error(t, err) + + _, err = lr.DONsForCapability(ctx, "act@1.0.0") + require.Error(t, err) + + _, err = lr.RawConfigForCapability(ctx, "act@1.0.0", 1) + require.Error(t, err) +} diff --git a/crecore/registry/metadata.go b/crecore/registry/metadata.go new file mode 100644 index 000000000..6da05778f --- /dev/null +++ b/crecore/registry/metadata.go @@ -0,0 +1,62 @@ +package registry + +import ( + "context" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + regserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/server" +) + +// Metadata adapts the Syncer to regserver.MetadataSource. +// +// The registry and its wire adapter live in chainlink-common; only the on-chain +// read lives here, because only it needs an EVM client and contract bindings. +// Each call resolves the current snapshot, so a sync that lands between calls is +// picked up without the registry holding a stale view. +func (s *Syncer) Metadata() regserver.MetadataSource { return metadataSource{s} } + +type metadataSource struct{ s *Syncer } + +var _ regserver.MetadataSource = metadataSource{} + +func (m metadataSource) LocalNode(ctx context.Context) (capabilities.Node, error) { + lr, err := m.s.Current() + if err != nil { + return capabilities.Node{}, err + } + return lr.LocalNode(ctx) +} + +func (m metadataSource) NodeByPeerID(ctx context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { + lr, err := m.s.Current() + if err != nil { + return capabilities.Node{}, err + } + return lr.NodeByPeerID(ctx, peerID) +} + +func (m metadataSource) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { + lr, err := m.s.Current() + if err != nil { + return nil, err + } + return lr.DONsForCapability(ctx, capabilityID) +} + +func (m metadataSource) DONByID(ctx context.Context, donID uint32) (capabilities.DON, error) { + lr, err := m.s.Current() + if err != nil { + return capabilities.DON{}, err + } + return lr.DONByID(ctx, donID) +} + +func (m metadataSource) RawConfigForCapability(ctx context.Context, capabilityID string, donID uint32) ([]byte, error) { + lr, err := m.s.Current() + if err != nil { + return nil, err + } + return lr.RawConfigForCapability(ctx, capabilityID, donID) +} diff --git a/crecore/registry/syncer.go b/crecore/registry/syncer.go new file mode 100644 index 000000000..3702c9d05 --- /dev/null +++ b/crecore/registry/syncer.go @@ -0,0 +1,298 @@ +package registry + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + capregv2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// DefaultSyncInterval matches chainlink's registrysyncer tick so a node fronted +// by crecore sees registry changes on the same cadence it did before. +const DefaultSyncInterval = 12 * time.Second + +// pageLimit bounds each view call. The v2 contract's getters are paginated; +// this reads one large page rather than implementing pagination, matching +// chainlink's registrysyncer/v2. If a registry ever exceeds it, the sync logs a +// warning rather than silently truncating. +const pageLimit = 1024 + +// contractCapabilityType is the on-chain capability type enum, in the +// CapabilitiesRegistry contract's ordering. +type contractCapabilityType uint8 + +const ( + contractCapabilityTypeTrigger contractCapabilityType = iota + contractCapabilityTypeAction + contractCapabilityTypeConsensus + contractCapabilityTypeTarget +) + +// capabilityMetadata is the JSON blob the v2 contract stores per capability. +type capabilityMetadata struct { + CapabilityType uint8 `json:"capabilityType"` + ResponseType uint8 `json:"responseType"` +} + +// registryCaller is the subset of the generated v2 wrapper this syncer uses. +// Narrowing it keeps the syncer testable without a chain. +type registryCaller interface { + GetCapabilities(opts *bind.CallOpts, start, limit *big.Int) ([]capregv2.CapabilitiesRegistryCapabilityInfo, error) + GetDONs(opts *bind.CallOpts, start, limit *big.Int) ([]capregv2.CapabilitiesRegistryDONInfo, error) + GetNodes(opts *bind.CallOpts, start, limit *big.Int) ([]capregv2.INodeInfoProviderNodeInfo, error) +} + +// Syncer polls the on-chain CapabilitiesRegistry and publishes immutable +// LocalRegistry snapshots. +// +// It polls rather than following events, for the same reason chainlink's syncer +// does: a whole-world snapshot cannot desync from a reorg or a dropped log +// subscription, so there is no missed-event failure mode to reason about. The +// cost is a fixed propagation delay bounded by the tick interval. +type Syncer struct { + services.StateMachine + + lggr logger.Logger + caller registryCaller + getPeerID func() (ragetypes.PeerID, error) + interval time.Duration + + // current holds the most recent successful snapshot, or nil before the + // first successful sync. + current atomic.Pointer[LocalRegistry] + + stopCh services.StopChan + done chan struct{} +} + +// NewSyncer builds a Syncer against an already-dialed EVM client. +// +// backend is typically chainlink-evm's multinode-backed client.Client, which +// satisfies bind.ContractCaller; the reliability behaviour (node health, dead +// node declaration, primary selection) comes from there rather than being +// reimplemented here. +func NewSyncer( + lggr logger.Logger, + backend bind.ContractCaller, + registryAddress common.Address, + getPeerID func() (ragetypes.PeerID, error), + interval time.Duration, +) (*Syncer, error) { + caller, err := capregv2.NewCapabilitiesRegistryCaller(registryAddress, backend) + if err != nil { + return nil, fmt.Errorf("failed to bind CapabilitiesRegistry at %s: %w", registryAddress, err) + } + if interval <= 0 { + interval = DefaultSyncInterval + } + return &Syncer{ + lggr: logger.Named(lggr, "RegistrySyncer"), + caller: caller, + getPeerID: getPeerID, + interval: interval, + stopCh: make(services.StopChan), + done: make(chan struct{}), + }, nil +} + +func (s *Syncer) Name() string { return s.lggr.Name() } + +func (s *Syncer) Start(context.Context) error { + return s.StartOnce("RegistrySyncer", func() error { + go s.syncLoop() + return nil + }) +} + +func (s *Syncer) Close() error { + return s.StopOnce("RegistrySyncer", func() error { + close(s.stopCh) + <-s.done + return nil + }) +} + +func (s *Syncer) HealthReport() map[string]error { + err := s.Healthy() + if s.current.Load() == nil { + err = errors.New("no successful registry sync yet") + } + return map[string]error{s.Name(): err} +} + +// Current returns the latest snapshot, or an error if none has landed yet. +func (s *Syncer) Current() (*LocalRegistry, error) { + lr := s.current.Load() + if lr == nil { + return nil, errors.New("registry not synced yet") + } + return lr, nil +} + +func (s *Syncer) syncLoop() { + defer close(s.done) + + ctx, cancel := s.stopCh.NewCtx() + defer cancel() + + ticker := time.NewTicker(s.interval) + defer ticker.Stop() + + // Sync once up front: a ticker first fires at T+interval, and every reader + // of this process blocks until the first snapshot exists. + if err := s.Sync(ctx); err != nil { + s.lggr.Errorw("initial registry sync failed", "err", err) + } + + for { + select { + case <-s.stopCh: + return + case <-ticker.C: + if err := s.Sync(ctx); err != nil { + s.lggr.Errorw("registry sync failed", "err", err) + } + } + } +} + +// Sync performs one full read and, on success, publishes a new snapshot. +func (s *Syncer) Sync(ctx context.Context) error { + lr, err := s.importOnchainRegistry(ctx) + if err != nil { + return err + } + s.current.Store(lr) + s.lggr.Debugw("registry synced", + "capabilities", len(lr.IDsToCapabilities), + "dons", len(lr.IDsToDONs), + "nodes", len(lr.IDsToNodes)) + return nil +} + +func (s *Syncer) importOnchainRegistry(ctx context.Context) (*LocalRegistry, error) { + opts := &bind.CallOpts{Context: ctx} + start, limit := big.NewInt(0), big.NewInt(pageLimit) + + caps, err := s.caller.GetCapabilities(opts, start, limit) + if err != nil { + return nil, fmt.Errorf("getCapabilities: %w", err) + } + s.warnIfTruncated("capabilities", len(caps)) + + idsToCapabilities := make(map[string]Capability, len(caps)) + for _, c := range caps { + if c.IsDeprecated { + continue + } + capType, err := parseCapabilityType(c.Metadata) + if err != nil { + s.lggr.Warnw("failed to parse capability metadata, skipping", + "capabilityID", c.CapabilityId, "err", err) + continue + } + idsToCapabilities[c.CapabilityId] = Capability{ID: c.CapabilityId, CapabilityType: capType} + } + + dons, err := s.caller.GetDONs(opts, start, limit) + if err != nil { + return nil, fmt.Errorf("getDONs: %w", err) + } + s.warnIfTruncated("dons", len(dons)) + + idsToDONs := make(map[DonID]DON, len(dons)) + for _, d := range dons { + cfgs := make(map[string][]byte, len(d.CapabilityConfigurations)) + for _, dc := range d.CapabilityConfigurations { + cfgs[dc.CapabilityId] = dc.Config + } + idsToDONs[DonID(d.Id)] = DON{ + DON: toDON(d), + CapabilityConfigurations: cfgs, + } + } + + nodes, err := s.caller.GetNodes(opts, start, limit) + if err != nil { + return nil, fmt.Errorf("getNodes: %w", err) + } + s.warnIfTruncated("nodes", len(nodes)) + + idsToNodes := make(map[ragetypes.PeerID]NodeInfo, len(nodes)) + for _, n := range nodes { + idsToNodes[n.P2pId] = NodeInfo{ + NodeOperatorID: n.NodeOperatorId, + ConfigCount: n.ConfigCount, + WorkflowDONID: n.WorkflowDONId, + Signer: n.Signer, + P2pID: n.P2pId, + EncryptionPublicKey: n.EncryptionPublicKey, + CsaKey: n.CsaKey, + CapabilityIDs: n.CapabilityIds, + } + } + + return NewLocalRegistry(s.lggr, s.getPeerID, idsToDONs, idsToNodes, idsToCapabilities), nil +} + +// warnIfTruncated makes a hit against pageLimit visible instead of letting a +// full page read as "that is all there is". +func (s *Syncer) warnIfTruncated(what string, n int) { + if n >= pageLimit { + s.lggr.Warnw("registry read hit the page limit; results may be truncated", + "what", what, "count", n, "limit", pageLimit) + } +} + +func toDON(d capregv2.CapabilitiesRegistryDONInfo) capabilities.DON { + members := make([]ragetypes.PeerID, 0, len(d.NodeP2PIds)) + for _, p := range d.NodeP2PIds { + members = append(members, p) + } + return capabilities.DON{ + Name: d.Name, + ID: d.Id, + Families: d.DonFamilies, + ConfigVersion: d.ConfigCount, + Members: members, + F: d.F, + IsPublic: d.IsPublic, + AcceptsWorkflows: d.AcceptsWorkflows, + Config: d.Config, + } +} + +func parseCapabilityType(metadata []byte) (capabilities.CapabilityType, error) { + if len(metadata) == 0 { + return capabilities.CapabilityTypeUnknown, errors.New("metadata is empty") + } + var meta capabilityMetadata + if err := json.Unmarshal(metadata, &meta); err != nil { + return capabilities.CapabilityTypeUnknown, fmt.Errorf("invalid metadata: %w", err) + } + switch contractCapabilityType(meta.CapabilityType) { + case contractCapabilityTypeTrigger: + return capabilities.CapabilityTypeTrigger, nil + case contractCapabilityTypeAction: + return capabilities.CapabilityTypeAction, nil + case contractCapabilityTypeConsensus: + return capabilities.CapabilityTypeConsensus, nil + case contractCapabilityTypeTarget: + return capabilities.CapabilityTypeTarget, nil + default: + return capabilities.CapabilityTypeUnknown, fmt.Errorf("unknown on-chain capability type %d", meta.CapabilityType) + } +} diff --git a/crecore/registry/syncer_test.go b/crecore/registry/syncer_test.go new file mode 100644 index 000000000..9071bc44e --- /dev/null +++ b/crecore/registry/syncer_test.go @@ -0,0 +1,309 @@ +package registry + +import ( + "context" + "errors" + "math/big" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + capregv2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// fakeCaller stands in for the generated v2 contract caller. +type fakeCaller struct { + mu sync.Mutex + + caps []capregv2.CapabilitiesRegistryCapabilityInfo + dons []capregv2.CapabilitiesRegistryDONInfo + nodes []capregv2.INodeInfoProviderNodeInfo + + capsErr error + donsErr error + nodesErr error + + calls int + // lastStart/lastLimit record the pagination arguments actually sent. + lastStart, lastLimit *big.Int +} + +func (f *fakeCaller) GetCapabilities(_ *bind.CallOpts, start, limit *big.Int) ([]capregv2.CapabilitiesRegistryCapabilityInfo, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.calls++ + f.lastStart, f.lastLimit = start, limit + return f.caps, f.capsErr +} + +func (f *fakeCaller) GetDONs(*bind.CallOpts, *big.Int, *big.Int) ([]capregv2.CapabilitiesRegistryDONInfo, error) { + f.mu.Lock() + defer f.mu.Unlock() + return f.dons, f.donsErr +} + +func (f *fakeCaller) GetNodes(*bind.CallOpts, *big.Int, *big.Int) ([]capregv2.INodeInfoProviderNodeInfo, error) { + f.mu.Lock() + defer f.mu.Unlock() + return f.nodes, f.nodesErr +} + +func (f *fakeCaller) callCount() int { + f.mu.Lock() + defer f.mu.Unlock() + return f.calls +} + +// newTestSyncer builds a Syncer around a fake caller, bypassing NewSyncer's +// contract binding (which needs a chain backend). +func newTestSyncer(t *testing.T, caller registryCaller) *Syncer { + t.Helper() + s := &Syncer{ + lggr: logger.Test(t), + caller: caller, + getPeerID: func() (ragetypes.PeerID, error) { return peer(1), nil }, + interval: time.Hour, + } + s.stopCh = make(chan struct{}) + s.done = make(chan struct{}) + return s +} + +func fullCaller() *fakeCaller { + p1, p2 := peer(1), peer(2) + return &fakeCaller{ + caps: []capregv2.CapabilitiesRegistryCapabilityInfo{ + {CapabilityId: "act@1.0.0", Metadata: []byte(`{"capabilityType":1}`)}, + {CapabilityId: "cron@1.0.0", Metadata: []byte(`{"capabilityType":0}`)}, + }, + dons: []capregv2.CapabilitiesRegistryDONInfo{{ + Id: 2, + ConfigCount: 5, + F: 1, + IsPublic: true, + AcceptsWorkflows: true, + NodeP2PIds: [][32]byte{p1, p2}, + DonFamilies: []string{"zone-a"}, + Name: "cap-don", + Config: []byte("don-config"), + CapabilityConfigurations: []capregv2.CapabilitiesRegistryCapabilityConfiguration{ + {CapabilityId: "act@1.0.0", Config: []byte("act-cfg")}, + }, + }}, + nodes: []capregv2.INodeInfoProviderNodeInfo{ + {NodeOperatorId: 11, ConfigCount: 1, WorkflowDONId: 2, P2pId: p1, Signer: [32]byte{0xaa}, CapabilityIds: []string{"act@1.0.0"}}, + {NodeOperatorId: 12, ConfigCount: 1, WorkflowDONId: 2, P2pId: p2}, + }, + } +} + +func TestSyncer_CurrentBeforeFirstSync(t *testing.T) { + s := newTestSyncer(t, &fakeCaller{}) + + _, err := s.Current() + require.Error(t, err) + assert.Contains(t, err.Error(), "not synced") + + // Health must report the missing snapshot, not "healthy with no data". + report := s.HealthReport() + require.Len(t, report, 1) + for _, err := range report { + require.Error(t, err) + } +} + +func TestSyncer_SyncBuildsSnapshot(t *testing.T) { + ctx := context.Background() + caller := fullCaller() + s := newTestSyncer(t, caller) + + require.NoError(t, s.Sync(ctx)) + + lr, err := s.Current() + require.NoError(t, err) + + require.Len(t, lr.IDsToCapabilities, 2) + assert.Equal(t, capabilities.CapabilityTypeAction, lr.IDsToCapabilities["act@1.0.0"].CapabilityType) + assert.Equal(t, capabilities.CapabilityTypeTrigger, lr.IDsToCapabilities["cron@1.0.0"].CapabilityType) + + require.Len(t, lr.IDsToDONs, 1) + don := lr.IDsToDONs[2] + assert.Equal(t, "cap-don", don.Name) + assert.Equal(t, uint8(1), don.F) + // ConfigVersion comes from the contract's ConfigCount. + assert.Equal(t, uint32(5), don.ConfigVersion) + assert.Equal(t, []string{"zone-a"}, don.Families) + assert.True(t, don.IsPublic) + assert.True(t, don.AcceptsWorkflows) + assert.Equal(t, []byte("don-config"), don.Config) + require.Len(t, don.Members, 2) + + // Capability config is carried through undecoded. + assert.Equal(t, []byte("act-cfg"), don.CapabilityConfigurations["act@1.0.0"]) + + require.Len(t, lr.IDsToNodes, 2) + assert.Equal(t, uint32(11), lr.IDsToNodes[peer(1)].NodeOperatorID) + assert.Equal(t, [32]byte{0xaa}, lr.IDsToNodes[peer(1)].Signer) + + // A built snapshot is immediately usable end to end. + node, err := lr.LocalNode(ctx) + require.NoError(t, err) + assert.Equal(t, uint32(2), node.WorkflowDON.ID) +} + +func TestSyncer_SyncSendsPagination(t *testing.T) { + ctx := context.Background() + caller := fullCaller() + s := newTestSyncer(t, caller) + + require.NoError(t, s.Sync(ctx)) + assert.Equal(t, int64(0), caller.lastStart.Int64()) + assert.Equal(t, int64(pageLimit), caller.lastLimit.Int64()) +} + +func TestSyncer_SkipsDeprecatedAndUnparseableCapabilities(t *testing.T) { + ctx := context.Background() + + caller := fullCaller() + caller.caps = append(caller.caps, + // Deprecated capabilities are still returned by the contract; treating + // them as live would let workflows resolve a retired capability. + capregv2.CapabilitiesRegistryCapabilityInfo{ + CapabilityId: "old@1.0.0", IsDeprecated: true, Metadata: []byte(`{"capabilityType":1}`), + }, + // One bad metadata blob must not fail the whole sync. + capregv2.CapabilitiesRegistryCapabilityInfo{CapabilityId: "garbage@1.0.0", Metadata: []byte("{")}, + capregv2.CapabilitiesRegistryCapabilityInfo{CapabilityId: "empty@1.0.0", Metadata: nil}, + capregv2.CapabilitiesRegistryCapabilityInfo{CapabilityId: "weird@1.0.0", Metadata: []byte(`{"capabilityType":99}`)}, + ) + + s := newTestSyncer(t, caller) + require.NoError(t, s.Sync(ctx)) + + lr, err := s.Current() + require.NoError(t, err) + assert.Len(t, lr.IDsToCapabilities, 2) + for _, skipped := range []string{"old@1.0.0", "garbage@1.0.0", "empty@1.0.0", "weird@1.0.0"} { + _, ok := lr.IDsToCapabilities[skipped] + assert.False(t, ok, "%s should have been skipped", skipped) + } +} + +func TestSyncer_SyncErrorsLeavePreviousSnapshot(t *testing.T) { + ctx := context.Background() + + caller := fullCaller() + s := newTestSyncer(t, caller) + require.NoError(t, s.Sync(ctx)) + + first, err := s.Current() + require.NoError(t, err) + + // A failed read must not replace a good snapshot with a partial one; the node + // keeps operating on the last known-good view until a read succeeds. + caller.mu.Lock() + caller.donsErr = errors.New("rpc down") + caller.mu.Unlock() + + require.Error(t, s.Sync(ctx)) + + second, err := s.Current() + require.NoError(t, err) + assert.Same(t, first, second) +} + +func TestSyncer_SyncPropagatesEachReadError(t *testing.T) { + ctx := context.Background() + + for _, tc := range []struct { + name string + set func(*fakeCaller) + want string + }{ + {"capabilities", func(c *fakeCaller) { c.capsErr = errors.New("boom") }, "getCapabilities"}, + {"dons", func(c *fakeCaller) { c.donsErr = errors.New("boom") }, "getDONs"}, + {"nodes", func(c *fakeCaller) { c.nodesErr = errors.New("boom") }, "getNodes"}, + } { + t.Run(tc.name, func(t *testing.T) { + caller := fullCaller() + tc.set(caller) + s := newTestSyncer(t, caller) + + err := s.Sync(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.want) + }) + } +} + +func TestSyncer_StartSyncsImmediately(t *testing.T) { + ctx := context.Background() + + // A ticker first fires at T+interval, and every reader blocks until the first + // snapshot exists, so Start must not wait a full interval. + caller := fullCaller() + s := newTestSyncer(t, caller) + s.interval = time.Hour + + require.NoError(t, s.Start(ctx)) + t.Cleanup(func() { require.NoError(t, s.Close()) }) + + require.Eventually(t, func() bool { + _, err := s.Current() + return err == nil + }, 10*time.Second, 10*time.Millisecond) + + assert.GreaterOrEqual(t, caller.callCount(), 1) + + report := s.HealthReport() + for _, err := range report { + require.NoError(t, err) + } +} + +func TestSyncer_CloseStopsLoop(t *testing.T) { + ctx := context.Background() + + s := newTestSyncer(t, fullCaller()) + require.NoError(t, s.Start(ctx)) + require.NoError(t, s.Close()) + + // Close must be idempotent-safe under the StateMachine contract: a second + // call reports an error rather than panicking on a closed channel. + require.Error(t, s.Close()) +} + +func TestParseCapabilityType(t *testing.T) { + for _, tc := range []struct { + name string + in []byte + want capabilities.CapabilityType + wantErr bool + }{ + {"trigger", []byte(`{"capabilityType":0}`), capabilities.CapabilityTypeTrigger, false}, + {"action", []byte(`{"capabilityType":1}`), capabilities.CapabilityTypeAction, false}, + {"consensus", []byte(`{"capabilityType":2}`), capabilities.CapabilityTypeConsensus, false}, + {"target", []byte(`{"capabilityType":3}`), capabilities.CapabilityTypeTarget, false}, + {"out of range", []byte(`{"capabilityType":9}`), capabilities.CapabilityTypeUnknown, true}, + {"empty", nil, capabilities.CapabilityTypeUnknown, true}, + {"malformed", []byte("nope"), capabilities.CapabilityTypeUnknown, true}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := parseCapabilityType(tc.in) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/crecore/registry_service.go b/crecore/registry_service.go new file mode 100644 index 000000000..a95efdeb4 --- /dev/null +++ b/crecore/registry_service.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "google.golang.org/grpc" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/crecore/registry" + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + + regserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/server" + + evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" +) + +// registryService owns the on-chain registry poller and serves the +// CapabilitiesRegistry gRPC service. +// +// There is no enable switch: this binary running is what enables the registry, +// and core does not start without it. So a missing or malformed contract address +// is a startup failure rather than a reason to run degraded. +// +// It attaches to the proxy's gRPC server rather than opening a listener of its +// own: a node that delegates rage networking to this process already has a +// connection to it, and a second address would be one more thing to configure and +// keep in sync for no gain. +type registryService struct { + services.Service + eng *services.Engine + + // contractAddress is the on-chain CapabilitiesRegistry (v2) address, and + // syncInterval how often it is re-read. Both come from CLI flags; see main.go. + contractAddress string + syncInterval time.Duration + + lggr logger.Logger + evm standalone.Dependency[evmclient.Client] + factories standalone.Dependency[*ocr.Factories] + + syncer *registry.Syncer + registry *regserver.Registry +} + +var _ services.Service = (*registryService)(nil) + +func newRegistryService( + contractAddress string, + syncInterval time.Duration, + lggr logger.Logger, + evm standalone.Dependency[evmclient.Client], + factories standalone.Dependency[*ocr.Factories], +) *registryService { + s := ®istryService{ + contractAddress: contractAddress, + syncInterval: syncInterval, + lggr: lggr, + evm: evm, + factories: factories, + // The Registry exists from construction so it can be attached to the + // proxy's gRPC server before either service starts. Its metadata source is + // installed later, in start. + registry: regserver.New(lggr), + } + s.Service, s.eng = services.Config{ + Name: "CapabilitiesRegistry", + Start: s.start, + Close: s.close, + }.NewServiceEngine(lggr) + return s +} + +func (s *registryService) start(ctx context.Context) error { + if !common.IsHexAddress(s.contractAddress) { + return fmt.Errorf("--capabilities-registry-address is required and must be a hex address, got %q", s.contractAddress) + } + + cl, err := s.evm.Get(ctx) + if err != nil { + return fmt.Errorf("failed to get evm client: %w", err) + } + + // The peer ID comes from the same keystore-backed identity the rage peer uses, + // so the node record this process resolves is the node it fronts. + factories, err := s.factories.Get(ctx) + if err != nil { + return fmt.Errorf("failed to get libocr factories: %w", err) + } + peerID := factories.PeerID + getPeerID := func() (ragetypes.PeerID, error) { return peerID, nil } + + syncer, err := registry.NewSyncer( + s.lggr, + cl, + common.HexToAddress(s.contractAddress), + getPeerID, + s.syncInterval, + ) + if err != nil { + return fmt.Errorf("failed to create registry syncer: %w", err) + } + s.syncer = syncer + s.registry.SetMetadata(syncer.Metadata()) + + if err := syncer.Start(ctx); err != nil { + return fmt.Errorf("failed to start registry syncer: %w", err) + } + + s.lggr.Infow("CapabilitiesRegistry started", + "contract", s.contractAddress, "syncInterval", s.syncInterval, "peerID", peerID.String()) + return nil +} + +func (s *registryService) close() error { + if s.syncer != nil { + return s.syncer.Close() + } + return nil +} + +// Register attaches the CapabilitiesRegistry service to a gRPC server. +// +// Safe to call before start: the Registry is built in the constructor, and its +// metadata RPCs return a "not ready" error until start installs the syncer and +// the first sync lands. That decouples this service's startup from the proxy's, +// which must register everything before it Serves. +func (s *registryService) Register(srv *grpc.Server) { + regserver.Register(srv, s.registry) +} From ac517be2a859953bb04043c229172811ec673599 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 6 Aug 2026 10:21:34 -0400 Subject: [PATCH 05/30] Harden the bootstrapper lib --- libs/go.mod | 4 +- libs/standalone/bootstrapper.go | 147 +++++++++++++++++++++++++--- libs/standalone/db/db_dependency.go | 4 - libs/standalone/pyroscope.go | 52 ++++++++++ libs/standalone/telemetry.go | 129 ++++++++++++++++++++---- 5 files changed, 302 insertions(+), 34 deletions(-) create mode 100644 libs/standalone/pyroscope.go diff --git a/libs/go.mod b/libs/go.mod index 6fe4a90e6..384016456 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -6,6 +6,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 + github.com/grafana/pyroscope-go v1.3.0 github.com/hashicorp/go-plugin v1.8.0 github.com/jackc/pgx/v5 v5.9.2 github.com/jmoiron/sqlx v1.4.0 @@ -22,6 +23,7 @@ require ( github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 @@ -128,7 +130,6 @@ require ( github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect - github.com/grafana/pyroscope-go v1.3.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect @@ -250,7 +251,6 @@ require ( go.mongodb.org/mongo-driver v1.17.9 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index 46d388d5f..6aa7147a4 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -5,6 +5,8 @@ package standalone import ( "context" "fmt" + "io" + "net/http" "os" "os/signal" "sync" @@ -17,6 +19,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/services/otelhealth" + "github.com/smartcontractkit/chainlink-common/pkg/services/promhealth" + + "github.com/grafana/pyroscope-go" ) // StandaloneConfig holds the process-wide dependencies the Bootstrapper provides @@ -27,13 +33,22 @@ type StandaloneConfig struct { // can parse and re-level the entries when this process runs under one, // while remaining plain JSON logs when run standalone. Logger logger.SugaredLogger + + // BeholderClient is the process-wide telemetry client, nil unless + // CL_TELEMETRY_ENDPOINT is configured. Exposed so a factory can build its + // own otel instruments (meters, tracers) consistent with the rest of the + // process, the same way a LOOP plugin would reach beholder.GetClient(). + BeholderClient *beholder.Client } type Bootstrapper struct { - root *cobra.Command - config *StandaloneConfig - commonConfig CommonConfig - beholderClient *beholder.Client // nil unless CL_TELEMETRY_ENDPOINT is configured + root *cobra.Command + config *StandaloneConfig + commonConfig CommonConfig + profiler *pyroscope.Profiler // nil unless CL_PYROSCOPE_SERVER_ADDRESS is configured + + closersMu sync.Mutex + closers []io.Closer // resolved dependency values that implement io.Closer } // NewBootstrapper creates a new Bootstrapper using the cobra command as its root. @@ -61,26 +76,64 @@ func NewBootstrapper(root *cobra.Command, opts ...Option) *Bootstrapper { slggr.Fatalf("Failed to start telemetry: %s", err) } - bs := &Bootstrapper{root: root, config: &StandaloneConfig{Logger: slggr}, beholderClient: beholderClient} + profiler, err := startProfiler(root.Name()) + if err != nil { + slggr.Fatalf("Failed to start profiler: %s", err) + } + + bs := &Bootstrapper{ + root: root, + config: &StandaloneConfig{Logger: slggr, BeholderClient: beholderClient}, + profiler: profiler, + } root.PersistentFlags().BoolVar(&bs.commonConfig.Fake, "fake", false, "use fake dependencies instead of real ones") return bs } -// close flushes telemetry and logs: the counterpart to the setup in NewBootstrapper. +// close closes resolved dependencies (in reverse resolution order), stops +// profiling, flushes telemetry, and syncs logs: the counterpart to the setup +// in NewBootstrapper and to dependency resolution during run. func (b *Bootstrapper) close() { - if b.beholderClient != nil { - b.config.Logger.ErrorIfFn(b.beholderClient.Close, "Failed to close beholder client") + b.closersMu.Lock() + closers := b.closers + b.closersMu.Unlock() + + for i := len(closers) - 1; i >= 0; i-- { + b.config.Logger.ErrorIfFn(closers[i].Close, "Failed to close dependency") + } + + if b.profiler != nil { + b.config.Logger.ErrorIfFn(b.profiler.Stop, "Failed to stop pyroscope profiler") + } + if b.config.BeholderClient != nil { + b.config.Logger.ErrorIfFn(b.config.BeholderClient.Close, "Failed to close beholder client") } _ = b.config.Logger.Sync() } +// registerCloser records v for closing on shutdown if it implements +// io.Closer. Safe to call concurrently. +func (b *Bootstrapper) registerCloser(v any) { + c, ok := v.(io.Closer) + if !ok { + return + } + b.closersMu.Lock() + b.closers = append(b.closers, c) + b.closersMu.Unlock() +} + // Logger returns the logger instance. It is safe to call before running the binary func (b *Bootstrapper) Logger() logger.SugaredLogger { return b.config.Logger } // run composes the services returned by factory into a single supervising // service via services.Engine sub-services, so their health is aggregated the // same way the rest of the stack does it (services.Config.NewSubServices + -// HealthReport). It starts them, then blocks until an interrupt, then closes. +// HealthReport). It starts them along with a health checker (registered +// against the aggregated root service) and, if CL_PROMETHEUS_PORT is set, a +// web server on that port serving /metrics, /debug/pprof, and /healthz + +// /readyz (backed by the health checker), then blocks until an interrupt, +// then closes everything in reverse. func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service) error { defer b.close() @@ -100,6 +153,28 @@ func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service) } defer func() { _ = root.Close() }() + checker, err := b.startHealthChecker(root) + if err != nil { + stop() + return err + } + defer func() { b.config.Logger.ErrorIfFn(checker.Close, "Failed to close health checker") }() + + if port, ok, err := prometheusPort(); err != nil { + stop() + return err + } else if ok { + http.HandleFunc("/healthz", healthHandler(checker.IsHealthy)) + http.HandleFunc("/readyz", healthHandler(checker.IsReady)) + + web := loop.WebServerOpts{}.New(b.config.Logger, port) + if err := web.Start(ctx); err != nil { + stop() + return fmt.Errorf("failed to start prometheus web server: %w", err) + } + defer func() { b.config.Logger.ErrorIfFn(web.Close, "Failed to close prometheus web server") }() + } + if underPluginHost() { // Launched by a go-plugin host (e.g. the core node): expose the empty // LOOP so the host can supervise this process over gRPC (handshake + @@ -122,6 +197,50 @@ func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service) return b.root.Execute() } +// healthHandler adapts a services.HealthChecker.IsHealthy/IsReady-shaped func into +// an HTTP handler: 200 with each check's status when ok, 503 and the failing +// checks' errors otherwise. +func healthHandler(check func() (bool, map[string]error)) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + ok, errs := check() + if !ok { + w.WriteHeader(http.StatusServiceUnavailable) + } + for name, err := range errs { + if err != nil { + fmt.Fprintf(w, "%s: %s\n", name, err) + } + } + if ok { + fmt.Fprintln(w, "ok") + } + } +} + +// startHealthChecker builds a services.HealthChecker that mirrors reporter (usually +// the aggregated root service) as prometheus metrics ("health", "uptime_seconds", +// "version") and, when telemetry is configured, as otel metrics through the same +// beholder client/meter the rest of the process uses. +func (b *Bootstrapper) startHealthChecker(reporter services.HealthReporter) (*services.HealthChecker, error) { + cfg := promhealth.ConfigureHooks(services.HealthCheckerConfig{}) + if bc := b.config.BeholderClient; bc != nil { + var err error + cfg, err = otelhealth.ConfigureHooks(cfg, bc.Meter) + if err != nil { + return nil, fmt.Errorf("failed to configure health checker otel hooks: %w", err) + } + } + + checker := cfg.New() + if err := checker.Start(); err != nil { + return nil, fmt.Errorf("failed to start health checker: %w", err) + } + if err := checker.Register(reporter); err != nil { + return nil, fmt.Errorf("failed to register health checker reporter: %w", err) + } + return checker, nil +} + // underPluginHost reports whether this process was launched by a go-plugin host, // detected via the empty plugin's handshake magic cookie. go-plugin's Serve // refuses to run (and exits) when this is absent, so we only serve the plugin in @@ -151,10 +270,16 @@ type BootstrapDependency[T any] interface { type dependency[T any] struct { bs *Bootstrapper bd BootstrapDependency[T] + + registerOnce sync.Once // guards registering the resolved value with bs, since Get may be called more than once } -func (d dependency[T]) Get(ctx context.Context) (T, error) { - return d.bd.Get(ctx, d.bs.commonConfig) +func (d *dependency[T]) Get(ctx context.Context) (T, error) { + v, err := d.bd.Get(ctx, d.bs.commonConfig) + if err == nil { + d.registerOnce.Do(func() { d.bs.registerCloser(v) }) + } + return v, err } // OnceBootstrapper wraps a BootstrapDependency so that Get is evaluated at most diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index e4da157ae..0714eaed5 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -57,10 +57,6 @@ func (d *dependency) AddCommands(command *cobra.Command) { standalone.BindWithEnvVar(f.Lookup("db-url")) } -func (d *dependency) Close() { - _ = d.db.Close() -} - func pgDb() (*sql.DB, error) { dbURL := os.Getenv(dbURLEnvVar) diff --git a/libs/standalone/pyroscope.go b/libs/standalone/pyroscope.go new file mode 100644 index 000000000..787b84b63 --- /dev/null +++ b/libs/standalone/pyroscope.go @@ -0,0 +1,52 @@ +package standalone + +import ( + "os" + "runtime/debug" + + "github.com/grafana/pyroscope-go" +) + +const ( + envPyroscopeServerAddress = "CL_PYROSCOPE_SERVER_ADDRESS" + envPyroscopeAuthToken = "CL_PYROSCOPE_AUTH_TOKEN" + envPyroscopeEnvironment = "CL_PYROSCOPE_ENVIRONMENT" +) + +// startProfiler starts continuous profiling to a Pyroscope server when +// CL_PYROSCOPE_SERVER_ADDRESS is configured, tagged with appName (typically +// the binary's cobra command name) plus build version/SHA. Returns a nil +// profiler and no error when unconfigured. +func startProfiler(appName string) (*pyroscope.Profiler, error) { + addr := os.Getenv(envPyroscopeServerAddress) + if addr == "" { + return nil, nil + } + + var ver, sha string + if bi, ok := debug.ReadBuildInfo(); ok { + ver = bi.Main.Version + sha = bi.Main.Sum + if len(sha) > 7 { + sha = sha[:7] + } + } + + return pyroscope.Start(pyroscope.Config{ + ApplicationName: appName, + ServerAddress: addr, + AuthToken: os.Getenv(envPyroscopeAuthToken), + Tags: map[string]string{ + "version": ver, + "sha": sha, + "environment": os.Getenv(envPyroscopeEnvironment), + }, + ProfileTypes: []pyroscope.ProfileType{ + pyroscope.ProfileCPU, + pyroscope.ProfileAllocObjects, + pyroscope.ProfileAllocSpace, + pyroscope.ProfileInuseObjects, + pyroscope.ProfileInuseSpace, + }, + }) +} diff --git a/libs/standalone/telemetry.go b/libs/standalone/telemetry.go index 30d7b4799..03dfdfc7b 100644 --- a/libs/standalone/telemetry.go +++ b/libs/standalone/telemetry.go @@ -1,9 +1,12 @@ package standalone -// This file copies the two small pieces of LOOP plugin setup that standalone -// binaries also need — the hclog-compatible logger and the CL_TELEMETRY_* env -// config for beholder — so that they behave the same with or without a plugin -// host, without depending on loop.Server and the full env contract it requires. +// This file copies the pieces of LOOP plugin setup that standalone binaries +// also need — the hclog-compatible logger, the CL_TELEMETRY_*/CL_TRACING_*/ +// CL_CHIP_INGRESS_* env config for beholder, and the prometheus web server +// port — so that they behave the same with or without a plugin host, without +// depending on loop.Server and the full env contract it requires. Tracing +// reuses loop.TracingConfig directly since it's a standalone helper, not part +// of that env contract. import ( "context" @@ -11,7 +14,9 @@ import ( "os" "strconv" "strings" + "time" + prombridge "go.opentelemetry.io/contrib/bridges/prometheus" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.uber.org/zap" @@ -19,17 +24,26 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/loop" ) // Env vars mirroring the loop.EnvConfig telemetry subset, set by the plugin // host (or by the operator, when running standalone). const ( - envTelemetryEndpoint = "CL_TELEMETRY_ENDPOINT" - envTelemetryInsecureConn = "CL_TELEMETRY_INSECURE_CONNECTION" - envTelemetryCACertFile = "CL_TELEMETRY_CA_CERT_FILE" - envTelemetryAttribute = "CL_TELEMETRY_ATTRIBUTE_" - envTelemetryAuthHeader = "CL_TELEMETRY_AUTH_HEADER" - envTelemetryAuthPubKeyHex = "CL_TELEMETRY_AUTH_PUB_KEY_HEX" + envTelemetryEndpoint = "CL_TELEMETRY_ENDPOINT" + envTelemetryInsecureConn = "CL_TELEMETRY_INSECURE_CONNECTION" + envTelemetryCACertFile = "CL_TELEMETRY_CA_CERT_FILE" + envTelemetryAttribute = "CL_TELEMETRY_ATTRIBUTE_" + envTelemetryAuthHeader = "CL_TELEMETRY_AUTH_HEADER" + envTelemetryAuthPubKeyHex = "CL_TELEMETRY_AUTH_PUB_KEY_HEX" + envTelemetryAuthHeadersTTL = "CL_TELEMETRY_AUTH_HEADERS_TTL" + envTelemetryPrometheusBridge = "CL_TELEMETRY_PROMETHEUS_BRIDGE_ENABLED" + envTracingEnabled = "CL_TRACING_ENABLED" + envTracingSamplingRatio = "CL_TRACING_SAMPLING_RATIO" + envTracingTLSCertFile = "CL_TRACING_TLS_CERT_FILE" + envChipIngressEndpoint = "CL_CHIP_INGRESS_ENDPOINT" + envChipIngressInsecureConn = "CL_CHIP_INGRESS_INSECURE_CONNECTION" + envPrometheusPort = "CL_PROMETHEUS_PORT" ) // Option configures optional Bootstrapper behavior. @@ -72,13 +86,10 @@ func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder. cfg := beholder.DefaultConfig() cfg.OtelExporterGRPCEndpoint = endpoint - cfg.InsecureConnection = false - if s := os.Getenv(envTelemetryInsecureConn); s != "" { - var err error - cfg.InsecureConnection, err = strconv.ParseBool(s) - if err != nil { - return nil, fmt.Errorf("failed to parse %s: %w", envTelemetryInsecureConn, err) - } + var err error + cfg.InsecureConnection, err = envBool(envTelemetryInsecureConn) + if err != nil { + return nil, err } cfg.CACertFile = os.Getenv(envTelemetryCACertFile) for k, v := range envMap(envTelemetryAttribute) { @@ -86,6 +97,12 @@ func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder. } cfg.AuthHeaders = envMap(envTelemetryAuthHeader) cfg.AuthPublicKeyHex = os.Getenv(envTelemetryAuthPubKeyHex) + if s := os.Getenv(envTelemetryAuthHeadersTTL); s != "" { + cfg.AuthHeadersTTL, err = time.ParseDuration(s) + if err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", envTelemetryAuthHeadersTTL, err) + } + } // Logs already reach their destination via stderr (parsed by the plugin // host when under one); don't stream them a second time. cfg.LogStreamingEnabled = false @@ -93,6 +110,55 @@ func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder. // client is created, so the views cannot be applied any later than this. cfg.MetricViews = otelViews + bridgeEnabled, err := envBool(envTelemetryPrometheusBridge) + if err != nil { + return nil, err + } + if bridgeEnabled { + // Feeds metrics already registered on the global prometheus registry + // (e.g. via promauto, like the health checker's) into the same OTLP + // pipeline, so they don't need a separate scrape target. + cfg.MetricProducers = append(cfg.MetricProducers, prombridge.NewMetricProducer()) + } + + cfg.ChipIngressEmitterGRPCEndpoint = os.Getenv(envChipIngressEndpoint) + cfg.ChipIngressEmitterEnabled = cfg.ChipIngressEmitterGRPCEndpoint != "" + if cfg.ChipIngressEmitterEnabled { + cfg.ChipIngressInsecureConnection, err = envBool(envChipIngressInsecureConn) + if err != nil { + return nil, err + } + } + + tracingEnabled, err := envBool(envTracingEnabled) + if err != nil { + return nil, err + } + if tracingEnabled { + tracingCfg := loop.TracingConfig{ + Enabled: true, + CollectorTarget: endpoint, + SamplingRatio: 1, + TLSCertPath: os.Getenv(envTracingTLSCertFile), + } + if cfg.AuthHeaders != nil { + tracingCfg.AuthHeaders = cfg.AuthHeaders + } + if s := os.Getenv(envTracingSamplingRatio); s != "" { + tracingCfg.SamplingRatio, err = strconv.ParseFloat(s, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", envTracingSamplingRatio, err) + } + } + + exporter, err := tracingCfg.NewSpanExporter() + if err != nil { + return nil, fmt.Errorf("failed to setup tracing exporter: %w", err) + } + cfg.TraceSpanExporter = exporter + cfg.TraceSampleRatio = tracingCfg.SamplingRatio + } + client, err := beholder.NewClient(cfg) if err != nil { return nil, fmt.Errorf("failed to create beholder client: %w", err) @@ -105,6 +171,35 @@ func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder. return client, nil } +// envBool parses key as a bool, defaulting to false when unset. +func envBool(key string) (bool, error) { + s := os.Getenv(key) + if s == "" { + return false, nil + } + b, err := strconv.ParseBool(s) + if err != nil { + return false, fmt.Errorf("failed to parse %s: %w", key, err) + } + return b, nil +} + +// prometheusPort reports the port for the /metrics and /debug/pprof web +// server from CL_PROMETHEUS_PORT. ok is false when unset, so the server +// stays off unless explicitly configured. Port 0 asks the OS for an +// ephemeral port, e.g. for tests. +func prometheusPort() (port int, ok bool, err error) { + s := os.Getenv(envPrometheusPort) + if s == "" { + return 0, false, nil + } + port, err = strconv.Atoi(s) + if err != nil { + return 0, false, fmt.Errorf("failed to parse %s: %w", envPrometheusPort, err) + } + return port, true, nil +} + // envMap collects env vars starting with prefix into a map, with the prefix // stripped from the keys, mirroring how the plugin host encodes map-valued // config (loop.EnvConfig.AsCmdEnv). Returns nil when none are set. From c19a859e1ecd3cebc54fd577378f94b60759ddc4 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 6 Aug 2026 10:28:26 -0400 Subject: [PATCH 06/30] Update libs in crecore --- crecore/go.mod | 2 +- crecore/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crecore/go.mod b/crecore/go.mod index 27e60817e..d024806dc 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -4,7 +4,7 @@ go 1.26.2 require ( github.com/ethereum/go-ethereum v1.17.3 - github.com/smartcontractkit/capabilities/libs v0.0.0-20260805175532-500ba79bfb4d + github.com/smartcontractkit/capabilities/libs v0.0.0-20260806142134-6ce85e626bf1 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 diff --git a/crecore/go.sum b/crecore/go.sum index 9d54954a8..5fbf39803 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -710,8 +710,8 @@ github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260805175532-500ba79bfb4d h1:SAGzQVHlpxRZrTd7znT4keD8RwvrL3dM0Tv2GHtuIyA= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260805175532-500ba79bfb4d/go.mod h1:hZFz3d31VSBzN8n7eGdSjRoge4Wxgp59Swj1Cf7Mc90= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260806142134-6ce85e626bf1 h1:evnfpGLpPS6igAjyl1LNNt3cVZ8ewo2u/0dqjWnOyQk= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260806142134-6ce85e626bf1/go.mod h1:Y+x7ksG6LxrJp8dLp5KTDIHJc1uspy4GIYHSpL9GEJQ= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 h1:XnMKhJDTl+JY+W7mIU1NJfaHqC4ykMXW0rz8hk9x5PQ= From f8955275f4a105d9540f8487cd3a5e4485ba39d8 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Fri, 7 Aug 2026 15:50:51 -0400 Subject: [PATCH 07/30] Update libs to use flags from common, may migrate the structs for DB etc later --- libs/go.mod | 4 +- libs/go.sum | 4 +- libs/standalone/bootstrapper.go | 20 ++++- libs/standalone/cmd_helpers.go | 24 ++---- libs/standalone/config_dependency.go | 3 + libs/standalone/db/db_dependency.go | 100 +++++++++++++++++------ libs/standalone/evm/dependency.go | 100 +++++++++++------------ libs/standalone/ocr/dependency.go | 117 +++++++++++++++------------ libs/standalone/ocr/keyring.go | 12 ++- 9 files changed, 226 insertions(+), 158 deletions(-) diff --git a/libs/go.mod b/libs/go.mod index 384016456..84d2ca200 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -13,14 +13,13 @@ require ( github.com/pressly/goose/v3 v3.27.1 github.com/prometheus/client_golang v1.23.2 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-common/keystore v1.3.0 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 - github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 @@ -223,6 +222,7 @@ require ( github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stellar/go-stellar-sdk v0.4.0 // indirect github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect diff --git a/libs/go.sum b/libs/go.sum index 096a902b7..0a0ef2a47 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -713,8 +713,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 h1:XnMKhJDTl+JY+W7mIU1NJfaHqC4ykMXW0rz8hk9x5PQ= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 h1:4GjFJWcytRCoLzmLr/QXWq/zokaZrP+lfS+zeIcP2zM= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510/go.mod h1:DUnczFmJPvNHsQc9er8wdBvt4bPJzPOgr62i7mQ1jhM= github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index 6aa7147a4..690a9da80 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/services" @@ -86,7 +87,12 @@ func NewBootstrapper(root *cobra.Command, opts ...Option) *Bootstrapper { config: &StandaloneConfig{Logger: slggr, BeholderClient: beholderClient}, profiler: profiler, } - root.PersistentFlags().BoolVar(&bs.commonConfig.Fake, "fake", false, "use fake dependencies instead of real ones") + // Registered through pkg/config/flags like every other config struct, so --fake is + // documented alongside the settings it interacts with rather than being invisible to the + // generated docs. It stays at the top level: it is process-wide, not one dependency's. + if err := flags.RegisterCommandFlags(root, &bs.commonConfig, flags.DefaultTOMLOptions("CRE", "CL")); err != nil { + slggr.Fatalf("Failed to register common flags: %s", err) + } return bs } @@ -251,7 +257,9 @@ func underPluginHost() bool { } type CommonConfig struct { - Fake bool + // Kept out of the example config: the example shows a real run, and --fake selects a + // different set of dependencies than the ones it illustrates. + Fake bool `toml:"fake" usage:"use fake dependencies instead of real ones" flagdocs:"noexample"` } type Dependency[T any] interface { @@ -260,6 +268,12 @@ type Dependency[T any] interface { type BootstrapCommand interface { AddCommands(*cobra.Command) + + // Namespace roots this dependency's configuration, so its settings group together and + // same-named settings from different dependencies don't collide - "database" gives + // --database.url, the key database.url and the env var CRE_DATABASE_URL. Return "" to + // keep the settings at the top level. + Namespace() string } type BootstrapDependency[T any] interface { @@ -314,3 +328,5 @@ func (o *onceBootstrapper[T]) Get(ctx context.Context, c CommonConfig) (T, error func (o *onceBootstrapper[T]) AddCommands(cmd *cobra.Command) { o.bd.AddCommands(cmd) } + +func (o *onceBootstrapper[T]) Namespace() string { return o.bd.Namespace() } diff --git a/libs/standalone/cmd_helpers.go b/libs/standalone/cmd_helpers.go index 4962be7be..fe0547ed3 100644 --- a/libs/standalone/cmd_helpers.go +++ b/libs/standalone/cmd_helpers.go @@ -1,18 +1,8 @@ +// Package standalone bootstraps single-binary CRE processes. +// +// Flag/env/config-file binding is handled by chainlink-common's pkg/config/flags: a +// dependency declares a tagged config struct and registers it with +// flags.RegisterCommandFlags(cmd, &cfg, "CRE", "CL") from AddCommands, which binds each field +// as a CLI flag, a viper key, and CRE_*/CL_* env vars, then decodes and validates it (see the +// `validate:"..."` tags) before the command runs. package standalone - -import ( - "fmt" - "strings" - - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -func BindWithEnvVar(flag *pflag.Flag) { - formatted := strings.ToUpper(strings.ReplaceAll(flag.Name, "-", "_")) - creEnv := "CRE_" + formatted - clEnv := "CL_" + formatted - flag.Usage += fmt.Sprintf("[env in order: %s, %s]", creEnv, clEnv) - _ = viper.BindPFlag(flag.Name, flag) - _ = viper.BindEnv(flag.Name, creEnv, clEnv) -} diff --git a/libs/standalone/config_dependency.go b/libs/standalone/config_dependency.go index 1576c366b..e1c7db8c6 100644 --- a/libs/standalone/config_dependency.go +++ b/libs/standalone/config_dependency.go @@ -26,6 +26,9 @@ func (c *configDependency[T]) Get(_ context.Context, _ CommonConfig) (T, error) return c.parse(content) } +// Namespace is empty: --config-file is a process-wide setting, not one dependency's. +func (c *configDependency[T]) Namespace() string { return "" } + func (c *configDependency[T]) AddCommands(command *cobra.Command) { command.PersistentFlags().StringVar(&c.file, "config-file", "", "specifies the config file to load from") } diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index 0714eaed5..24a370f01 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -5,40 +5,72 @@ import ( "database/sql" "fmt" "io/fs" - "os" // Register the pgx database/sql driver under the name "pgx". _ "github.com/jackc/pgx/v5/stdlib" - "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" + + "github.com/smartcontractkit/capabilities/libs/standalone" ) -const dbURLEnvVar = "CL_DATABASE_URL" +// fakeFlag is the bootstrapper's --fake flag (see standalone.NewBootstrapper). It belongs to a +// different config struct than this one, so the rules below can't be `validate` tags - those +// only see sibling fields - and are checked in Config.validate against the value read back out +// of viper instead. +const fakeFlag = "fake" + +// Config is the database configuration. Its two settings are related to --fake, which this +// struct does not own: +// +// - real-db only means something in fake mode, so it is rejected without --fake. +// - database-url is needed whenever a real database is actually used: always in normal mode, +// and in fake mode only when --real-db asks for one. +// +// Neither rule is expressible as a `validate` tag (both reference --fake), so neither shows up +// in the generated docs on its own - the usage text spells them out so the docs still explain +// when each setting applies. +type Config struct { + URL string `toml:"url" usage:"database url; required unless running with --fake and without --real-db" example:"'postgresql://user:password@localhost:5432/chainlink?sslmode=disable'"` + + // Kept out of the example config: the example shows a normal run against a real database, + // where this setting does not apply. It is still documented. + UseRealDBForFake bool `toml:"real-db" usage:"use a real database even though --fake is set; requires --fake, and a url to point at" flagdocs:"noexample"` +} + +// validate enforces the --fake relationships described on Config. +func (c Config) validate(fake bool) error { + if c.UseRealDBForFake && !fake { + return fmt.Errorf("--database.real-db only applies in fake mode: pass --fake as well, or drop --database.real-db to use a real database normally") + } + if (c.UseRealDBForFake || !fake) && c.URL == "" { + return fmt.Errorf("--database.url is required when a real database is used (it is only optional with --fake and without --real-db)") + } + return nil +} func Dependency(migrationsFS fs.FS, migrationTable string) standalone.BootstrapDependency[*sql.DB] { - // Wrap in OnceBootstrapper so Get (which opens the DB and runs migrations) - // runs at most once even if several services resolve this dependency. return standalone.OnceBootstrapper[*sql.DB](&dependency{migrationsFS: migrationsFS, migrationTable: migrationTable}) } type dependency struct { - db *sql.DB - url string - useRealDbForFake bool - migrationsFS fs.FS - migrationTable string + db *sql.DB + cfg Config + migrationsFS fs.FS + migrationTable string } func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConfig) (*sql.DB, error) { - dbFn := pgDb - if commonConfig.Fake && !d.useRealDbForFake { + if commonConfig.Fake && !d.cfg.UseRealDBForFake { // TODO set db to an in-memory one // Also add a subcommand to override with real DB even if fake is used } var err error - d.db, err = dbFn() + d.db, err = sql.Open("pgx", d.cfg.URL) if err != nil { return nil, err } @@ -50,21 +82,41 @@ func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConf return d.db, nil } -func (d *dependency) AddCommands(command *cobra.Command) { - f := command.PersistentFlags() - f.BoolVar(&d.useRealDbForFake, "real-db", false, "uses a real db even if fake is set for the program") - f.StringVar(&d.url, "db-url", "", "database url") - standalone.BindWithEnvVar(f.Lookup("db-url")) -} +// Namespace groups the database settings under database.* (--database.url, CRE_DATABASE_URL). +func (d *dependency) Namespace() string { return "database" } -func pgDb() (*sql.DB, error) { +func (d *dependency) AddCommands(command *cobra.Command) { + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = d.Namespace() + if err := flags.RegisterCommandFlags(command, &d.cfg, opts); err != nil { + panic(err) + } - dbURL := os.Getenv(dbURLEnvVar) - if dbURL == "" { - return nil, fmt.Errorf("%s must be set", dbURLEnvVar) + // --fake is owned by the bootstrapper, so bind it into viper to read it back here the same + // way any other setting is resolved (flag, then CRE_FAKE / CL_FAKE). + if f := command.PersistentFlags().Lookup(fakeFlag); f != nil { + _ = viper.BindPFlag(fakeFlag, f) + _ = viper.BindEnv(fakeFlag, "CRE_FAKE", "CL_FAKE") } - return sql.Open("pgx", dbURL) + // Chain after whatever is already wired (notably the decode step RegisterCommandFlags just + // installed), so cfg is populated by the time these cross-flag rules are checked - and + // checked before any service starts, rather than at connect time. + prev := command.PersistentPreRunE + command.PersistentPreRunE = func(c *cobra.Command, args []string) error { + if prev != nil { + if err := prev(c, args); err != nil { + return err + } + } + // help and completion describe the program rather than run it, so they must work + // without a usable configuration - the library skips its own validation for them, and + // this check has to do the same. + if flags.IsBuiltinCommand(c) { + return nil + } + return d.cfg.validate(viper.GetBool(fakeFlag)) + } } var _ standalone.BootstrapDependency[*sql.DB] = (*dependency)(nil) diff --git a/libs/standalone/evm/dependency.go b/libs/standalone/evm/dependency.go index afd5f91a7..3c3141383 100644 --- a/libs/standalone/evm/dependency.go +++ b/libs/standalone/evm/dependency.go @@ -1,28 +1,15 @@ -// Package evm provides a standalone.BootstrapDependency that supplies an EVM RPC -// client to a standalone binary. -// -// The client is chainlink-evm's multinode-backed client.Client, not a bare geth -// *ethclient.Client. That is deliberate: multinode is where the RPC reliability -// behaviour lives (per-node health polling, sync-threshold detection, dead-node -// declaration, primary selection, load-balanced RPC support). A standalone process -// reading a contract needs exactly those properties and should not reimplement -// them. -// -// What this does not bring along is the relayer / ContractReader stack. Callers -// get a client.Client and make bind.ContractCaller view calls against generated -// gethwrappers directly, which is a far shorter path to follow than -// relayer -> ContractReader -> codec -> ReadIdentifier. package evm import ( "context" - "errors" "fmt" "math/big" "time" "github.com/spf13/cobra" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" "github.com/smartcontractkit/chainlink-common/pkg/logger" evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" @@ -54,7 +41,7 @@ const ( // Dependency returns a standalone.BootstrapDependency that resolves a dialed, // multinode-backed EVM client. // -// At least one --evm-http-url is required. WebSocket URLs are optional; without +// At least one --evm.http-url is required. WebSocket URLs are optional; without // them the client polls for heads rather than subscribing, which is enough for // view calls. func Dependency(lggr logger.Logger) standalone.BootstrapDependency[evmclient.Client] { @@ -63,65 +50,72 @@ func Dependency(lggr logger.Logger) standalone.BootstrapDependency[evmclient.Cli return standalone.OnceBootstrapper[evmclient.Client](&dependency{lggr: lggr}) } +// Config is the EVM client configuration. At least one http-url is required; WebSocket URLs +// are optional, and without them the client polls for heads rather than subscribing, which is +// enough for view calls. +type Config struct { + HTTPURLs []string `toml:"http-url" usage:"EVM RPC HTTP URL(s); repeat or comma-separate for a multinode pool" validate:"required" example:"['https://rpc.example.com']"` + WSURLs []string `toml:"ws-url" usage:"EVM RPC WebSocket URL(s), positionally paired with --evm.http-url; optional" validate:"excluded_without=HTTPURLs"` + ChainID string `toml:"chain-id" usage:"EVM chain ID" validate:"required" example:"'1'"` + ChainType string `toml:"chain-type" usage:"EVM chain type (empty for a generic EVM chain)"` + + FinalityTagEnabled bool `toml:"finality-tag-enabled" usage:"use the finalized block tag instead of a finality depth"` + FinalityDepth uint32 `toml:"finality-depth" usage:"finality depth, used when --evm.finality-tag-enabled=false"` + PollInterval config.Duration `toml:"poll-interval" usage:"per-node health poll interval"` +} + +var defaultConfig = Config{ + FinalityTagEnabled: true, + FinalityDepth: defaultFinalityDepth, + PollInterval: *config.MustNewDuration(defaultPollInterval), +} + type dependency struct { lggr logger.Logger client evmclient.Client - - httpURLs []string - wsURLs []string - chainID string - chainType string - finalityTagEnabled bool - finalityDepth uint32 - pollInterval time.Duration + cfg Config } var _ standalone.BootstrapDependency[evmclient.Client] = (*dependency)(nil) +// Namespace groups the EVM settings under evm.* (--evm.http-url, CRE_EVM_HTTP_URL). +func (d *dependency) Namespace() string { return "evm" } + func (d *dependency) AddCommands(cmd *cobra.Command) { - f := cmd.PersistentFlags() - f.StringSliceVar(&d.httpURLs, "evm-http-url", nil, "EVM RPC HTTP URL(s); repeat or comma-separate for a multinode pool") - f.StringSliceVar(&d.wsURLs, "evm-ws-url", nil, "EVM RPC WebSocket URL(s), positionally paired with --evm-http-url; optional") - f.StringVar(&d.chainID, "evm-chain-id", "", "EVM chain ID") - f.StringVar(&d.chainType, "evm-chain-type", "", "EVM chain type (empty for a generic EVM chain)") - f.BoolVar(&d.finalityTagEnabled, "evm-finality-tag-enabled", true, "use the finalized block tag instead of a finality depth") - f.Uint32Var(&d.finalityDepth, "evm-finality-depth", defaultFinalityDepth, "finality depth, used when --evm-finality-tag-enabled=false") - f.DurationVar(&d.pollInterval, "evm-poll-interval", defaultPollInterval, "per-node health poll interval") - - standalone.BindWithEnvVar(f.Lookup("evm-http-url")) - standalone.BindWithEnvVar(f.Lookup("evm-ws-url")) - standalone.BindWithEnvVar(f.Lookup("evm-chain-id")) + d.cfg = defaultConfig + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = d.Namespace() + if err := flags.RegisterCommandFlags(cmd, &d.cfg, opts); err != nil { + panic(err) + } } func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmclient.Client, error) { - if len(d.httpURLs) == 0 { - return nil, errors.New("at least one --evm-http-url is required") - } - if len(d.wsURLs) > 0 && len(d.wsURLs) != len(d.httpURLs) { - return nil, fmt.Errorf("--evm-ws-url count (%d) must match --evm-http-url count (%d) when provided", len(d.wsURLs), len(d.httpURLs)) + if len(d.cfg.WSURLs) > 0 && len(d.cfg.WSURLs) != len(d.cfg.HTTPURLs) { + return nil, fmt.Errorf("--evm.ws-url count (%d) must match --evm.http-url count (%d) when provided", len(d.cfg.WSURLs), len(d.cfg.HTTPURLs)) } - chainID, ok := new(big.Int).SetString(d.chainID, 10) + chainID, ok := new(big.Int).SetString(d.cfg.ChainID, 10) if !ok { - return nil, fmt.Errorf("invalid --evm-chain-id %q", d.chainID) + return nil, fmt.Errorf("invalid --evm.chain-id %q", d.cfg.ChainID) } - nodeCfgs := make([]evmclient.NodeConfig, len(d.httpURLs)) - for i := range d.httpURLs { + nodeCfgs := make([]evmclient.NodeConfig, len(d.cfg.HTTPURLs)) + for i := range d.cfg.HTTPURLs { name := fmt.Sprintf("node-%d", i) order := int32(1) sendOnly := false loadBalanced := false cfg := evmclient.NodeConfig{ Name: &name, - HTTPURL: &d.httpURLs[i], + HTTPURL: &d.cfg.HTTPURLs[i], Order: &order, SendOnly: &sendOnly, IsLoadBalancedRPC: &loadBalanced, } - if len(d.wsURLs) > 0 { - cfg.WSURL = &d.wsURLs[i] + if len(d.cfg.WSURLs) > 0 { + cfg.WSURL = &d.cfg.WSURLs[i] } nodeCfgs[i] = cfg } @@ -131,8 +125,8 @@ func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmcli pollSuccessThreshold := defaultPollSuccessThreshold syncThreshold := defaultSyncThreshold nodeIsSyncingEnabled := false - finalityDepth := d.finalityDepth - finalityTagEnabled := d.finalityTagEnabled + finalityDepth := d.cfg.FinalityDepth + finalityTagEnabled := d.cfg.FinalityTagEnabled safeTagSupported := false finalizedBlockOffset := defaultFinalizedBlockOffset enforceRepeatableRead := true @@ -141,11 +135,11 @@ func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmcli chainCfg, nodePool, nodes, err := evmclient.NewClientConfigs( &selectionMode, defaultLeaseDuration, - d.chainType, + d.cfg.ChainType, nodeCfgs, &pollFailureThreshold, &pollSuccessThreshold, - d.pollInterval, + d.cfg.PollInterval.Duration(), &syncThreshold, &nodeIsSyncingEnabled, defaultNoNewHeadsThreshold, @@ -167,7 +161,7 @@ func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmcli // clientErrors is nil: it only feeds ClassifySendError on the transaction send // path, and this client is read-only. - cl, err := evmclient.NewEvmClient(nodePool, chainCfg, nil, d.lggr, chainID, nodes, chaintype.ChainType(d.chainType)) + cl, err := evmclient.NewEvmClient(nodePool, chainCfg, nil, d.lggr, chainID, nodes, chaintype.ChainType(d.cfg.ChainType)) if err != nil { return nil, fmt.Errorf("failed to create evm client: %w", err) } diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index a6b906ec5..d733037ce 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -5,20 +5,19 @@ // It has two mutually exclusive modes, mirroring core's SingletonPeerWrapper: // // - create: build a local libocr peer (networking.NewPeer) and expose its -// factories. Requires --listen-addresses and uses the node's P2P identity +// factories. Requires --ocr.listen-addresses and uses the node's P2P identity // and OCR discoverer table from the database. // - proxy: delegate rage networking to an out-of-process proxy at -// --proxy-address, exposing proxy-client-backed factories instead of a +// --ocr.proxy-address, exposing proxy-client-backed factories instead of a // local peer. // // The two modes are wired as a cobra "one of" set: exactly one of -// --listen-addresses / --proxy-address may (and must) be provided. +// --ocr.listen-addresses / --ocr.proxy-address may (and must) be provided. package ocr import ( "context" "database/sql" - "errors" "fmt" "io" "time" @@ -32,6 +31,8 @@ import ( ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" commonocr "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" @@ -78,47 +79,65 @@ func Dependency(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql return standalone.OnceBootstrapper[*Factories](&dependency{lggr: lggr, db: db, discovererTable: discovererTable}) } +// Config is the libocr networking configuration. +// +// The two modes are expressed as validator tags rather than cobra's +// MarkFlagsMutuallyExclusive/MarkFlagsOneRequired: those only inspect whether a flag was +// literally typed on the command line, so they would reject a mode that was selected via a +// config file or env var. required_without/excluded_with are checked against the decoded +// values instead, so exactly-one-of holds no matter which source supplied it. +type Config struct { + // create-mode config + ListenAddresses []string `toml:"listen-addresses" usage:"rage p2p V2 listen addresses (host:port); creates a local peer" validate:"required_without=ProxyAddress,excluded_with=ProxyAddress" example:"['127.0.0.1:1234']"` + AnnounceAddresses []string `toml:"announce-addresses" usage:"rage p2p V2 announce addresses (host:port); defaults to the listen addresses" validate:"excluded_without=ListenAddresses"` + DeltaReconcile config.Duration `toml:"delta-reconcile" usage:"rage p2p V2 delta reconcile interval"` + DeltaDial config.Duration `toml:"delta-dial" usage:"rage p2p V2 minimum interval between dial attempts"` + + IncomingBufferSize int `toml:"incoming-buffer-size" usage:"per-remote incoming message buffer size"` + OutgoingBufferSize int `toml:"outgoing-buffer-size" usage:"per-remote outgoing message buffer size"` + + // KeystorePassword unlocks the node's key ring, which is where the shared P2P identity + // comes from. Both modes need it: the peer ID is what other DON members expect at this + // address, whether this process runs the peer itself or delegates to a proxy. Typed as a + // SecretString so it redacts itself in logs, docs and the example config. + KeystorePassword config.SecretString `toml:"keystore-password" usage:"password for the node keystore holding the shared P2P identity" validate:"required"` + + // proxy-mode config + ProxyAddress string `toml:"proxy-address" usage:"delegate rage networking to a proxy at this gRPC address instead of creating a local peer" validate:"excluded_with=ListenAddresses"` +} + +var defaultConfig = Config{ + DeltaReconcile: *config.MustNewDuration(time.Minute), + DeltaDial: *config.MustNewDuration(5 * time.Second), + IncomingBufferSize: 100, + OutgoingBufferSize: 100, +} + type dependency struct { lggr commonlogger.Logger db standalone.BootstrapDependency[*sql.DB] discovererTable string - // create-mode config - listenAddresses []string - announceAddresses []string - deltaReconcile time.Duration - deltaDial time.Duration - incomingBufferSize int - outgoingBufferSize int - - // proxy-mode config - proxyAddress string + cfg Config } var _ standalone.BootstrapDependency[*Factories] = (*dependency)(nil) +// Namespace groups the libocr networking settings under ocr.* (--ocr.listen-addresses, +// CRE_OCR_LISTEN_ADDRESSES). +func (d *dependency) Namespace() string { return "ocr" } + func (d *dependency) AddCommands(cmd *cobra.Command) { // The database is a create/proxy-shared input (P2P identity, discoverer - // table), so surface its flags too. + // table), so surface its flags too - under its own namespace. d.db.AddCommands(cmd) - f := cmd.PersistentFlags() - // create-mode flags - f.StringSliceVar(&d.listenAddresses, "listen-addresses", nil, "rage p2p V2 listen addresses (host:port); creates a local peer") - f.StringSliceVar(&d.announceAddresses, "announce-addresses", nil, "rage p2p V2 announce addresses (host:port); defaults to the listen addresses") - f.DurationVar(&d.deltaReconcile, "delta-reconcile", time.Minute, "rage p2p V2 delta reconcile interval") - f.DurationVar(&d.deltaDial, "delta-dial", 5*time.Second, "rage p2p V2 minimum interval between dial attempts") - f.IntVar(&d.incomingBufferSize, "incoming-buffer-size", 100, "per-remote incoming message buffer size") - f.IntVar(&d.outgoingBufferSize, "outgoing-buffer-size", 100, "per-remote outgoing message buffer size") - // proxy-mode flag - f.StringVar(&d.proxyAddress, "proxy-address", "", "delegate rage networking to a proxy at this gRPC address instead of creating a local peer") - - // Exactly one mode: --listen-addresses (create) xor --proxy-address (proxy). - cmd.MarkFlagsMutuallyExclusive("listen-addresses", "proxy-address") - cmd.MarkFlagsOneRequired("listen-addresses", "proxy-address") - // announce-addresses is optional in create mode (libocr defaults it to the - // listen addresses) but is meaningless in proxy mode. - cmd.MarkFlagsMutuallyExclusive("announce-addresses", "proxy-address") + d.cfg = defaultConfig + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = d.Namespace() + if err := flags.RegisterCommandFlags(cmd, &d.cfg, opts); err != nil { + panic(err) + } } func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Factories, error) { @@ -130,13 +149,13 @@ func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Fact // Both modes use the node's own P2P identity so this process is the same // peer as the node it fronts. - keyring, err := loadPeerKeyring(ctx, ds) + keyring, err := loadPeerKeyring(ctx, ds, string(d.cfg.KeystorePassword)) if err != nil { return nil, err } peerID := ragetypes.PeerIDFromKeyring(keyring) - if d.proxyAddress != "" { + if d.cfg.ProxyAddress != "" { return d.proxyFactories(peerID) } return d.localFactories(ds, keyring, peerID) @@ -144,29 +163,25 @@ func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Fact // localFactories builds a real libocr peer and exposes its factories. func (d *dependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*Factories, error) { - if len(d.listenAddresses) == 0 { - return nil, errors.New("at least one --listen-addresses is required") - } - discovererDB := commonocr.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) d.lggr.Infow("Creating local p2p peer", "peerID", peerID.String(), - "listenAddresses", d.listenAddresses, - "announceAddresses", d.announceAddresses, + "listenAddresses", d.cfg.ListenAddresses, + "announceAddresses", d.cfg.AnnounceAddresses, ) peer, err := networking.NewPeer(networking.PeerConfig{ PeerKeyring: keyring, Logger: commonlogger.NewOCRWrapper(d.lggr, false, func(string) {}), - V2ListenAddresses: d.listenAddresses, - V2AnnounceAddresses: d.announceAddresses, - V2DeltaReconcile: d.deltaReconcile, - V2DeltaDial: d.deltaDial, + V2ListenAddresses: d.cfg.ListenAddresses, + V2AnnounceAddresses: d.cfg.AnnounceAddresses, + V2DeltaReconcile: d.cfg.DeltaReconcile.Duration(), + V2DeltaDial: d.cfg.DeltaDial.Duration(), V2DiscovererDatabase: discovererDB, V2EndpointConfig: networking.EndpointConfigV2{ - IncomingMessageBufferSize: d.incomingBufferSize, - OutgoingMessageBufferSize: d.outgoingBufferSize, + IncomingMessageBufferSize: d.cfg.IncomingBufferSize, + OutgoingMessageBufferSize: d.cfg.OutgoingBufferSize, }, MetricsRegisterer: prometheus.DefaultRegisterer, LatencyMetricsServiceConfigs: rageping.DefaultConfigs(), @@ -186,26 +201,26 @@ func (d *dependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, // proxyFactories delegates rage networking to an out-of-process proxy: no local // peer is created; the factories are backed by proxy clients connected to -// d.proxyAddress. The node's raw peer ID is passed to the endpoint factories, +// d.cfg.ProxyAddress. The node's raw peer ID is passed to the endpoint factories, // as libocr compares it against the peer IDs in the OCR config. func (d *dependency) proxyFactories(peerID ragetypes.PeerID) (*Factories, error) { - endpointFactory, err := creproxy.NewProxyEndpointFactory(peerID.String(), d.proxyAddress) + endpointFactory, err := creproxy.NewProxyEndpointFactory(peerID.String(), d.cfg.ProxyAddress) if err != nil { return nil, fmt.Errorf("failed to create proxy OCR endpoint factory: %w", err) } - endpoint2Factory, err := creproxy.NewProxyEndpoint2Factory(peerID.String(), d.proxyAddress) + endpoint2Factory, err := creproxy.NewProxyEndpoint2Factory(peerID.String(), d.cfg.ProxyAddress) if err != nil { _ = endpointFactory.Close() return nil, fmt.Errorf("failed to create proxy OCR3.1 endpoint factory: %w", err) } - pgFactory, err := creproxy.NewProxyPeerGroupFactory(d.proxyAddress) + pgFactory, err := creproxy.NewProxyPeerGroupFactory(d.cfg.ProxyAddress) if err != nil { _ = endpointFactory.Close() _ = endpoint2Factory.Close() return nil, fmt.Errorf("failed to create proxy peer group factory: %w", err) } - d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.proxyAddress, "peerID", peerID.String()) + d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.cfg.ProxyAddress, "peerID", peerID.String()) return &Factories{ OCR2Endpoint: endpointFactory, diff --git a/libs/standalone/ocr/keyring.go b/libs/standalone/ocr/keyring.go index 78a600279..2be0aa214 100644 --- a/libs/standalone/ocr/keyring.go +++ b/libs/standalone/ocr/keyring.go @@ -6,7 +6,6 @@ import ( "crypto/rand" "errors" "fmt" - "os" "github.com/jmoiron/sqlx" @@ -15,10 +14,6 @@ import ( "github.com/smartcontractkit/chainlink-common/keystore/corekeys/models" ) -// keystorePasswordEnvVar is the keystore password used to decrypt the node's -// key ring. This process shares the node's DB (CL_DATABASE_URL) and password. -const keystorePasswordEnvVar = "CL_PASSWORD_KEYSTORE" - // loadPeerKeyring loads the P2P key from the node's keystore so this process // uses the SAME peer identity as the node it fronts (other DON members expect // this node's peer ID at this address). It reads the node's existing encrypted @@ -27,15 +22,18 @@ const keystorePasswordEnvVar = "CL_PASSWORD_KEYSTORE" // deliberately small copy of core's keyManager.Unlock using only // chainlink-common packages, so this binary needn't import chainlink core. // +// password is the node's keystore password: this process shares the node's +// database and therefore its keystore password. +// // TODO: drop this once the keystore is migrated to chainlink-common's // keystore.Keystore + pgstore (as chainlink-ccv already uses), after which we // can LoadKeystore from the shared table directly. -func loadPeerKeyring(ctx context.Context, ds *sqlx.DB) (*peerKeyring, error) { +func loadPeerKeyring(ctx context.Context, ds *sqlx.DB, password string) (*peerKeyring, error) { var encrypted []byte if err := ds.GetContext(ctx, &encrypted, "SELECT encrypted_keys FROM encrypted_key_rings LIMIT 1"); err != nil { return nil, fmt.Errorf("failed to read node key ring: %w", err) } - kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(os.Getenv(keystorePasswordEnvVar)) + kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(password) if err != nil { return nil, fmt.Errorf("failed to decrypt node key ring: %w", err) } From ecce3886ea9f4a7e353c723e7444078f464b3d67 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Fri, 7 Aug 2026 15:56:51 -0400 Subject: [PATCH 08/30] Update core to use the new config stuff --- crecore/docs/CONFIG.md | 199 +++++++++++++++++++++++++++++++++++++++ crecore/go.mod | 4 +- crecore/go.sum | 8 +- crecore/main.go | 44 ++++----- crecore/proxy_service.go | 22 ++++- 5 files changed, 241 insertions(+), 36 deletions(-) create mode 100644 crecore/docs/CONFIG.md diff --git a/crecore/docs/CONFIG.md b/crecore/docs/CONFIG.md new file mode 100644 index 000000000..b7ca4b273 --- /dev/null +++ b/crecore/docs/CONFIG.md @@ -0,0 +1,199 @@ +# main Configuration + +## Example + +```toml +# ----- Global Configuration ----- +proxy-listen-address = ':50051' +capabilities-registry-address = '0xYourRegistryAddress' +capabilities-registry-sync-interval = '12s' +[database] +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' +[ocr] +listen-addresses = ['127.0.0.1:1234'] +delta-reconcile = '1m0s' +delta-dial = '5s' +incoming-buffer-size = 100 +outgoing-buffer-size = 100 +keystore-password = 'xxxxx' +[evm] +http-url = ['https://rpc.example.com'] +chain-id = '1' +chain-type = '' +finality-tag-enabled = true +finality-depth = 50 +poll-interval = '10s' + + +``` + +## Global +```toml +proxy-listen-address = ':50051' # Default +capabilities-registry-address = '0xYourRegistryAddress' # Example +capabilities-registry-sync-interval = '12s' # Default +``` + + +# Global Configuration + +### proxy-listen-address +```toml +proxy-listen-address = ':50051' # Default +``` +proxy-listen-address address the proxy gRPC server listens on + +### capabilities-registry-address +```toml +capabilities-registry-address = '0xYourRegistryAddress' # Example +``` +capabilities-registry-address on-chain CapabilitiesRegistry (v2) contract address + +### capabilities-registry-sync-interval +```toml +capabilities-registry-sync-interval = '12s' # Default +``` +capabilities-registry-sync-interval how often the on-chain registry is re-read + +### fake +```toml +fake = false # Docs only +``` +fake use fake dependencies instead of real ones + +## database +```toml +[database] +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +``` + + +### url +```toml +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +``` +url database url; required unless running with --fake and without --real-db + +### real-db +```toml +real-db = false # Docs only +``` +real-db use a real database even though --fake is set; requires --fake, and a url to point at + +## ocr +```toml +[ocr] +listen-addresses = ['127.0.0.1:1234'] # Example +announce-addresses = [] # Default +delta-reconcile = '1m0s' # Default +delta-dial = '5s' # Default +incoming-buffer-size = 100 # Default +outgoing-buffer-size = 100 # Default +keystore-password = 'xxxxx' # Example +proxy-address = '' # Default +``` + + +### listen-addresses +```toml +listen-addresses = ['127.0.0.1:1234'] # Example +``` +listen-addresses rage p2p V2 listen addresses (host:port); creates a local peer (required unless proxy-address is set; must not be set when proxy-address is set) + +### announce-addresses +```toml +announce-addresses = [] # Default +``` +announce-addresses rage p2p V2 announce addresses (host:port); defaults to the listen addresses (must not be set unless listen-addresses is set) + +### delta-reconcile +```toml +delta-reconcile = '1m0s' # Default +``` +delta-reconcile rage p2p V2 delta reconcile interval + +### delta-dial +```toml +delta-dial = '5s' # Default +``` +delta-dial rage p2p V2 minimum interval between dial attempts + +### incoming-buffer-size +```toml +incoming-buffer-size = 100 # Default +``` +incoming-buffer-size per-remote incoming message buffer size + +### outgoing-buffer-size +```toml +outgoing-buffer-size = 100 # Default +``` +outgoing-buffer-size per-remote outgoing message buffer size + +### keystore-password +```toml +keystore-password = 'xxxxx' # Example +``` +keystore-password password for the node keystore holding the shared P2P identity + +### proxy-address +```toml +proxy-address = '' # Default +``` +proxy-address delegate rage networking to a proxy at this gRPC address instead of creating a local peer (must not be set when listen-addresses is set) + +## evm +```toml +[evm] +http-url = ['https://rpc.example.com'] # Example +ws-url = [] # Default +chain-id = '1' # Example +chain-type = '' # Default +finality-tag-enabled = true # Default +finality-depth = 50 # Default +poll-interval = '10s' # Default +``` + + +### http-url +```toml +http-url = ['https://rpc.example.com'] # Example +``` +http-url EVM RPC HTTP URL(s); repeat or comma-separate for a multinode pool + +### ws-url +```toml +ws-url = [] # Default +``` +ws-url EVM RPC WebSocket URL(s), positionally paired with --evm.http-url; optional (must not be set unless http-url is set) + +### chain-id +```toml +chain-id = '1' # Example +``` +chain-id EVM chain ID + +### chain-type +```toml +chain-type = '' # Default +``` +chain-type EVM chain type (empty for a generic EVM chain) + +### finality-tag-enabled +```toml +finality-tag-enabled = true # Default +``` +finality-tag-enabled use the finalized block tag instead of a finality depth + +### finality-depth +```toml +finality-depth = 50 # Default +``` +finality-depth finality depth, used when --evm.finality-tag-enabled=false + +### poll-interval +```toml +poll-interval = '10s' # Default +``` +poll-interval per-node health poll interval + diff --git a/crecore/go.mod b/crecore/go.mod index d024806dc..74ed8179c 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -4,8 +4,8 @@ go 1.26.2 require ( github.com/ethereum/go-ethereum v1.17.3 - github.com/smartcontractkit/capabilities/libs v0.0.0-20260806142134-6ce85e626bf1 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 + github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d diff --git a/crecore/go.sum b/crecore/go.sum index 5fbf39803..190d10094 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -710,12 +710,12 @@ github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260806142134-6ce85e626bf1 h1:evnfpGLpPS6igAjyl1LNNt3cVZ8ewo2u/0dqjWnOyQk= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260806142134-6ce85e626bf1/go.mod h1:Y+x7ksG6LxrJp8dLp5KTDIHJc1uspy4GIYHSpL9GEJQ= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400 h1:V+CUSgMC6z4jySgI2PSpmbUaVu03go+gY3AYAFTK1NY= +github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400/go.mod h1:LexpT2LlA33BVwb4hKw5DHbnG9S+lHzS0HOkEOxLZ7U= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49 h1:XnMKhJDTl+JY+W7mIU1NJfaHqC4ykMXW0rz8hk9x5PQ= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260805174449-86b0bd040b49/go.mod h1:+ygsaXb7Us63JhgPc+MWy48ydNz4zAT3g/vpYEvRo2k= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 h1:4GjFJWcytRCoLzmLr/QXWq/zokaZrP+lfS+zeIcP2zM= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510/go.mod h1:DUnczFmJPvNHsQc9er8wdBvt4bPJzPOgr62i7mQ1jhM= github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= diff --git a/crecore/main.go b/crecore/main.go index 927b3d779..c73e4888f 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -4,7 +4,6 @@ import ( "context" "embed" "log" - "time" "github.com/spf13/cobra" @@ -13,8 +12,7 @@ import ( "github.com/smartcontractkit/capabilities/libs/standalone/evm" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" - "github.com/smartcontractkit/capabilities/crecore/registry" - + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" "github.com/smartcontractkit/chainlink-common/pkg/services" evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" ) @@ -35,35 +33,28 @@ func main() { } func run() error { - cfg := &Config{} - - var ( - registryAddress string - registrySyncInterval time.Duration - ) + cfg := defaultConfig root := &cobra.Command{ Use: "main", Short: "P2P proxy for the CRE", Long: `Runs a single shared rage (libocr) peer and exposes it over gRPC so that core can delegate its OCR networking (and, in future, DON-to-DON networking) to -this process. The peer's identity is loaded from the node's keystore (shared DB -via CL_DATABASE_URL, decrypted with CL_PASSWORD_KEYSTORE). - -Provide exactly one networking mode: --listen-addresses to run a local libocr -peer, or --proxy-address to delegate to another proxy. +this process. The peer's identity is the node's own, loaded from the keystore in +the database the two share. It also serves the CapabilitiesRegistry on that same gRPC address, read directly from chain with an EVM client (no relayer). Core uses that in place of its own -registrysyncer whenever it is delegating rage networking to this process, so ---capabilities-registry-address is required: this process running is what enables -the registry, and core does not start without it.`, +registrysyncer whenever it is delegating rage networking to this process, which +is why the registry address is required: this process running is what enables the +registry, and core does not start without it. + +Settings can come from flags, from CRE_/CL_ env vars, or from a --config file; +run "docs" to write the full reference to docs/CONFIG.md.`, } + root.PersistentFlags().String("config", "", "Path to config file") - root.PersistentFlags().StringVar(&cfg.ProxyListenAddress, "proxy-listen-address", ":50051", "address the proxy gRPC server listens on") - root.PersistentFlags().StringVar(®istryAddress, "capabilities-registry-address", "", "on-chain CapabilitiesRegistry (v2) contract address (required)") - root.PersistentFlags().DurationVar(®istrySyncInterval, "capabilities-registry-sync-interval", registry.DefaultSyncInterval, "how often the on-chain registry is re-read") - if err := root.MarkPersistentFlagRequired("capabilities-registry-address"); err != nil { + if err := flags.RegisterCommandFlags(root, &cfg, flags.DefaultTOMLOptions("CRE", "CL")); err != nil { return err } @@ -71,12 +62,13 @@ the registry, and core does not start without it.`, lggr := bootstrapper.Logger() // The ocr dependency owns the libocr networking config (create vs proxy - // mode) and wraps the database dependency it needs for the P2P identity and - // OCR discoverer table. + // mode, and the keystore password that unlocks the peer identity) and wraps + // the database dependency it needs for that identity and the OCR discoverer + // table. dbDep := db.Dependency(embeddedMigrations, migrationsTable) ocrDep := ocr.Dependency(lggr.Named("OCR"), dbDep, ocrDiscovererTable) // The registry always runs, so the EVM client is always resolved and the - // --evm-* flags are as required in practice as the registry address is. + // evm settings are as required in practice as the registry address is. evmDep := evm.Dependency(lggr.Named("EVM")) return standalone.Run2(bootstrapper, func( @@ -85,11 +77,11 @@ the registry, and core does not start without it.`, factories standalone.Dependency[*ocr.Factories], evmClient standalone.Dependency[evmclient.Client], ) []services.Service { - regSvc := newRegistryService(registryAddress, registrySyncInterval, + regSvc := newRegistryService(cfg.RegistryAddress, cfg.RegistrySyncInterval.Duration(), scfg.Logger.Named("capabilities registry"), evmClient, factories) // The registry attaches to the proxy's gRPC server, so core reaches both // over the single --proxy-listen-address it already configures. - proxySvc := newProxyService(cfg, scfg.Logger.Named("proxy service"), factories, regSvc.Register) + proxySvc := newProxyService(&cfg, scfg.Logger.Named("proxy service"), factories, regSvc.Register) return []services.Service{proxySvc, regSvc} }, ocrDep, evmDep) } diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index 08151d23a..181d7b77f 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -8,20 +8,34 @@ import ( "google.golang.org/grpc" + "github.com/smartcontractkit/capabilities/crecore/registry" "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" ) -// Config is the proxy gRPC server configuration, populated from CLI flags (see -// main.go). The libocr peer / proxy-client configuration lives on the ocr -// bootstrap dependency instead. +// Config is the root command's configuration, populated by flags.RegisterCommandFlags (see +// main.go). The libocr peer / proxy-client configuration lives on the ocr bootstrap dependency +// instead. type Config struct { // ProxyListenAddress is the address the proxy gRPC server listens on. - ProxyListenAddress string + ProxyListenAddress string `toml:"proxy-listen-address" usage:"address the proxy gRPC server listens on"` + + // RegistryAddress is the on-chain CapabilitiesRegistry (v2) contract address. The registry + // always runs, so this is as required in practice as the registry itself. + RegistryAddress string `toml:"capabilities-registry-address" usage:"on-chain CapabilitiesRegistry (v2) contract address" validate:"required" example:"'0xYourRegistryAddress'"` + + // RegistrySyncInterval is how often the on-chain registry is re-read. + RegistrySyncInterval config.Duration `toml:"capabilities-registry-sync-interval" usage:"how often the on-chain registry is re-read"` +} + +var defaultConfig = Config{ + ProxyListenAddress: ":50051", + RegistrySyncInterval: *config.MustNewDuration(registry.DefaultSyncInterval), } // proxyService exposes the libocr rage networking factories over gRPC so that From 29fe69e4fa6215aea57b954578e90c94b6618fac Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Mon, 10 Aug 2026 10:47:59 -0400 Subject: [PATCH 09/30] small bootstrapper cleanup --- crecore/go.mod | 6 +++ crecore/go.sum | 6 --- libs/go.mod | 4 ++ libs/go.sum | 4 -- libs/standalone/bootstrap_gen.go | 75 ++++----------------------- libs/standalone/bootstrapper.go | 47 +++++++++++------ libs/standalone/config_dependency.go | 34 ------------ libs/standalone/db/db_dependency.go | 67 ++++-------------------- libs/standalone/evm/dependency.go | 18 +++---- libs/standalone/gen/bootstrap.go.tmpl | 3 +- libs/standalone/ocr/dependency.go | 19 +++---- 11 files changed, 75 insertions(+), 208 deletions(-) delete mode 100644 libs/standalone/config_dependency.go diff --git a/crecore/go.mod b/crecore/go.mod index 74ed8179c..3e321a6e3 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -291,3 +291,9 @@ require ( pgregory.net/rapid v1.1.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common + +replace github.com/smartcontractkit/chainlink-evm => ../../chainlink-evm + +replace github.com/smartcontractkit/capabilities/libs => ../libs diff --git a/crecore/go.sum b/crecore/go.sum index 190d10094..b91fd8ef4 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -710,18 +710,12 @@ github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400 h1:V+CUSgMC6z4jySgI2PSpmbUaVu03go+gY3AYAFTK1NY= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400/go.mod h1:LexpT2LlA33BVwb4hKw5DHbnG9S+lHzS0HOkEOxLZ7U= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 h1:4GjFJWcytRCoLzmLr/QXWq/zokaZrP+lfS+zeIcP2zM= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510/go.mod h1:DUnczFmJPvNHsQc9er8wdBvt4bPJzPOgr62i7mQ1jhM= github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 h1:0vivai99gZjV92RLlxnIhUBTODg08JfdBD8wLfN8A5A= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872/go.mod h1:xxdndutJLTdMDdJwazJDCmBh8H/d7nwc4aj7ZqVbzN4= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 h1:JFo7C3FilwhfwGBLAyj2umbL+P4QxGmVi/b8yt9kqvI= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6/go.mod h1:a260YnLyWq2NHLUN5cSVyMGk9nhO6RguCaTI2rsVqyA= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb h1:HQN56hEteWOBagVDCxV2Fn++frjRI7dfnozEQUq/5ok= diff --git a/libs/go.mod b/libs/go.mod index 84d2ca200..4a4cdb254 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -292,3 +292,7 @@ require ( pgregory.net/rapid v1.1.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common + +replace github.com/smartcontractkit/chainlink-evm => ../../chainlink-evm diff --git a/libs/go.sum b/libs/go.sum index 0a0ef2a47..c5ceac35d 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -713,14 +713,10 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 h1:4GjFJWcytRCoLzmLr/QXWq/zokaZrP+lfS+zeIcP2zM= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510/go.mod h1:DUnczFmJPvNHsQc9er8wdBvt4bPJzPOgr62i7mQ1jhM= github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 h1:0vivai99gZjV92RLlxnIhUBTODg08JfdBD8wLfN8A5A= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872/go.mod h1:xxdndutJLTdMDdJwazJDCmBh8H/d7nwc4aj7ZqVbzN4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb h1:HQN56hEteWOBagVDCxV2Fn++frjRI7dfnozEQUq/5ok= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 h1:N1Q6NHuH+T2b6cst43LD2pnykNJSymP/szJJ7rrYY8I= diff --git a/libs/standalone/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go index 5017633ac..a2124e24f 100644 --- a/libs/standalone/bootstrap_gen.go +++ b/libs/standalone/bootstrap_gen.go @@ -18,12 +18,11 @@ func Run1[T0 any]( bootDep0 BootstrapDependency[T0], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0) - }) + }, bootDep0) } // Run2 bootstraps the services built from 2 dependencies. @@ -37,15 +36,13 @@ func Run2[T0 any, T1 any]( bootDep1 BootstrapDependency[T1], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1) - }) + }, bootDep0, bootDep1) } // Run3 bootstraps the services built from 3 dependencies. @@ -60,18 +57,15 @@ func Run3[T0 any, T1 any, T2 any]( bootDep2 BootstrapDependency[T2], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2) - }) + }, bootDep0, bootDep1, bootDep2) } // Run4 bootstraps the services built from 4 dependencies. @@ -87,21 +81,17 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( bootDep3 BootstrapDependency[T3], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} - bootDep3.AddCommands(bs.root) dep3 := &dependency[T3]{bd: bootDep3, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2, dep3) - }) + }, bootDep0, bootDep1, bootDep2, bootDep3) } // Run5 bootstraps the services built from 5 dependencies. @@ -118,24 +108,19 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( bootDep4 BootstrapDependency[T4], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} - bootDep3.AddCommands(bs.root) dep3 := &dependency[T3]{bd: bootDep3, bs: bs} - bootDep4.AddCommands(bs.root) dep4 := &dependency[T4]{bd: bootDep4, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4) - }) + }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4) } // Run6 bootstraps the services built from 6 dependencies. @@ -153,27 +138,21 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( bootDep5 BootstrapDependency[T5], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} - bootDep3.AddCommands(bs.root) dep3 := &dependency[T3]{bd: bootDep3, bs: bs} - bootDep4.AddCommands(bs.root) dep4 := &dependency[T4]{bd: bootDep4, bs: bs} - bootDep5.AddCommands(bs.root) dep5 := &dependency[T5]{bd: bootDep5, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5) - }) + }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5) } // Run7 bootstraps the services built from 7 dependencies. @@ -192,30 +171,23 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( bootDep6 BootstrapDependency[T6], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} - bootDep3.AddCommands(bs.root) dep3 := &dependency[T3]{bd: bootDep3, bs: bs} - bootDep4.AddCommands(bs.root) dep4 := &dependency[T4]{bd: bootDep4, bs: bs} - bootDep5.AddCommands(bs.root) dep5 := &dependency[T5]{bd: bootDep5, bs: bs} - bootDep6.AddCommands(bs.root) dep6 := &dependency[T6]{bd: bootDep6, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6) - }) + }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6) } // Run8 bootstraps the services built from 8 dependencies. @@ -235,33 +207,25 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( bootDep7 BootstrapDependency[T7], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} - bootDep3.AddCommands(bs.root) dep3 := &dependency[T3]{bd: bootDep3, bs: bs} - bootDep4.AddCommands(bs.root) dep4 := &dependency[T4]{bd: bootDep4, bs: bs} - bootDep5.AddCommands(bs.root) dep5 := &dependency[T5]{bd: bootDep5, bs: bs} - bootDep6.AddCommands(bs.root) dep6 := &dependency[T6]{bd: bootDep6, bs: bs} - bootDep7.AddCommands(bs.root) dep7 := &dependency[T7]{bd: bootDep7, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7) - }) + }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7) } // Run9 bootstraps the services built from 9 dependencies. @@ -282,36 +246,27 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any bootDep8 BootstrapDependency[T8], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} - bootDep3.AddCommands(bs.root) dep3 := &dependency[T3]{bd: bootDep3, bs: bs} - bootDep4.AddCommands(bs.root) dep4 := &dependency[T4]{bd: bootDep4, bs: bs} - bootDep5.AddCommands(bs.root) dep5 := &dependency[T5]{bd: bootDep5, bs: bs} - bootDep6.AddCommands(bs.root) dep6 := &dependency[T6]{bd: bootDep6, bs: bs} - bootDep7.AddCommands(bs.root) dep7 := &dependency[T7]{bd: bootDep7, bs: bs} - bootDep8.AddCommands(bs.root) dep8 := &dependency[T8]{bd: bootDep8, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) - }) + }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8) } // Run10 bootstraps the services built from 10 dependencies. @@ -333,37 +288,27 @@ func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 an bootDep9 BootstrapDependency[T9], ) error { - bootDep0.AddCommands(bs.root) dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - bootDep1.AddCommands(bs.root) dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - bootDep2.AddCommands(bs.root) dep2 := &dependency[T2]{bd: bootDep2, bs: bs} - bootDep3.AddCommands(bs.root) dep3 := &dependency[T3]{bd: bootDep3, bs: bs} - bootDep4.AddCommands(bs.root) dep4 := &dependency[T4]{bd: bootDep4, bs: bs} - bootDep5.AddCommands(bs.root) dep5 := &dependency[T5]{bd: bootDep5, bs: bs} - bootDep6.AddCommands(bs.root) dep6 := &dependency[T6]{bd: bootDep6, bs: bs} - bootDep7.AddCommands(bs.root) dep7 := &dependency[T7]{bd: bootDep7, bs: bs} - bootDep8.AddCommands(bs.root) dep8 := &dependency[T8]{bd: bootDep8, bs: bs} - bootDep9.AddCommands(bs.root) dep9 := &dependency[T9]{bd: bootDep9, bs: bs} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8, dep9) - }) + }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9) } diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index 690a9da80..d42a656d1 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -140,9 +140,13 @@ func (b *Bootstrapper) Logger() logger.SugaredLogger { return b.config.Logger } // web server on that port serving /metrics, /debug/pprof, and /healthz + // /readyz (backed by the health checker), then blocks until an interrupt, // then closes everything in reverse. -func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service) error { +func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service, commands ...BootstrapCommand) error { defer b.close() + if err := b.setupCommands(commands); err != nil { + return err + } + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -203,6 +207,23 @@ func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service) return b.root.Execute() } +func (b *Bootstrapper) setupCommands(commands []BootstrapCommand) error { + allCommands := make([]BootstrapCommand, 0, len(commands)) + for _, cmd := range commands { + allCommands = append(allCommands, cmd) + allCommands = append(allCommands, cmd.Dependencies()...) + } + + for _, cmd := range allCommands { + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = cmd.Namespace() + if err := flags.RegisterCommandFlags(b.root, cmd.Config(), opts); err != nil { + return err + } + } + return nil +} + // healthHandler adapts a services.HealthChecker.IsHealthy/IsReady-shaped func into // an HTTP handler: 200 with each check's status when ok, 503 and the failing // checks' errors otherwise. @@ -267,7 +288,8 @@ type Dependency[T any] interface { } type BootstrapCommand interface { - AddCommands(*cobra.Command) + Config() any + Dependencies() []BootstrapCommand // Namespace roots this dependency's configuration, so its settings group together and // same-named settings from different dependencies don't collide - "database" gives @@ -297,36 +319,27 @@ func (d *dependency[T]) Get(ctx context.Context) (T, error) { } // OnceBootstrapper wraps a BootstrapDependency so that Get is evaluated at most -// once: the first call resolves the dependency and caches its (value, error), +// onceGet: the first call resolves the dependency and caches its (value, error), // and every subsequent call returns that same result without re-running Get -// (the ctx and CommonConfig of later calls are ignored). AddCommands is -// delegated unchanged. +// (the ctx and CommonConfig of later calls are ignored). Other commands are all delegated // // BootstrapDependency implementations are shared and may have Get called more -// than once — e.g. one dependency resolving another, or the same dependency +// than onceGet — e.g. one dependency resolving another, or the same dependency // feeding several services — so a New function should wrap its dependency with // OnceBootstrapper before returning it, making repeated Get calls safe and // side-effect-free. func OnceBootstrapper[T any](bd BootstrapDependency[T]) BootstrapDependency[T] { - return &onceBootstrapper[T]{bd: bd} + return &onceBootstrapper[T]{BootstrapDependency: bd} } type onceBootstrapper[T any] struct { - bd BootstrapDependency[T] + BootstrapDependency[T] once sync.Once val T err error } func (o *onceBootstrapper[T]) Get(ctx context.Context, c CommonConfig) (T, error) { - o.once.Do(func() { - o.val, o.err = o.bd.Get(ctx, c) - }) + o.once.Do(func() { o.val, o.err = o.BootstrapDependency.Get(ctx, c) }) return o.val, o.err } - -func (o *onceBootstrapper[T]) AddCommands(cmd *cobra.Command) { - o.bd.AddCommands(cmd) -} - -func (o *onceBootstrapper[T]) Namespace() string { return o.bd.Namespace() } diff --git a/libs/standalone/config_dependency.go b/libs/standalone/config_dependency.go deleted file mode 100644 index e1c7db8c6..000000000 --- a/libs/standalone/config_dependency.go +++ /dev/null @@ -1,34 +0,0 @@ -package standalone - -import ( - "context" - "os" - - "github.com/spf13/cobra" -) - -type configDependency[T any] struct { - parse func([]byte) (T, error) - file string -} - -func NewConfigDependency[T any](parse func([]byte) (T, error)) BootstrapDependency[T] { - return OnceBootstrapper[T](&configDependency[T]{parse: parse}) -} - -func (c *configDependency[T]) Get(_ context.Context, _ CommonConfig) (T, error) { - content, err := os.ReadFile(c.file) - if err != nil { - var t T - return t, err - } - - return c.parse(content) -} - -// Namespace is empty: --config-file is a process-wide setting, not one dependency's. -func (c *configDependency[T]) Namespace() string { return "" } - -func (c *configDependency[T]) AddCommands(command *cobra.Command) { - command.PersistentFlags().StringVar(&c.file, "config-file", "", "specifies the config file to load from") -} diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index 24a370f01..4882592d8 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -9,20 +9,9 @@ import ( // Register the pgx database/sql driver under the name "pgx". _ "github.com/jackc/pgx/v5/stdlib" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/smartcontractkit/chainlink-common/pkg/config/flags" - "github.com/smartcontractkit/capabilities/libs/standalone" ) -// fakeFlag is the bootstrapper's --fake flag (see standalone.NewBootstrapper). It belongs to a -// different config struct than this one, so the rules below can't be `validate` tags - those -// only see sibling fields - and are checked in Config.validate against the value read back out -// of viper instead. -const fakeFlag = "fake" - // Config is the database configuration. Its two settings are related to --fake, which this // struct does not own: // @@ -41,17 +30,6 @@ type Config struct { UseRealDBForFake bool `toml:"real-db" usage:"use a real database even though --fake is set; requires --fake, and a url to point at" flagdocs:"noexample"` } -// validate enforces the --fake relationships described on Config. -func (c Config) validate(fake bool) error { - if c.UseRealDBForFake && !fake { - return fmt.Errorf("--database.real-db only applies in fake mode: pass --fake as well, or drop --database.real-db to use a real database normally") - } - if (c.UseRealDBForFake || !fake) && c.URL == "" { - return fmt.Errorf("--database.url is required when a real database is used (it is only optional with --fake and without --real-db)") - } - return nil -} - func Dependency(migrationsFS fs.FS, migrationTable string) standalone.BootstrapDependency[*sql.DB] { return standalone.OnceBootstrapper[*sql.DB](&dependency{migrationsFS: migrationsFS, migrationTable: migrationTable}) } @@ -63,8 +41,19 @@ type dependency struct { migrationTable string } +func (d *dependency) Config() any { + return &d.cfg +} + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{} +} + func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConfig) (*sql.DB, error) { if commonConfig.Fake && !d.cfg.UseRealDBForFake { + if d.cfg.URL != "" { + return nil, fmt.Errorf("database url set when using fake database") + } // TODO set db to an in-memory one // Also add a subcommand to override with real DB even if fake is used } @@ -85,38 +74,4 @@ func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConf // Namespace groups the database settings under database.* (--database.url, CRE_DATABASE_URL). func (d *dependency) Namespace() string { return "database" } -func (d *dependency) AddCommands(command *cobra.Command) { - opts := flags.DefaultTOMLOptions("CRE", "CL") - opts.Namespace = d.Namespace() - if err := flags.RegisterCommandFlags(command, &d.cfg, opts); err != nil { - panic(err) - } - - // --fake is owned by the bootstrapper, so bind it into viper to read it back here the same - // way any other setting is resolved (flag, then CRE_FAKE / CL_FAKE). - if f := command.PersistentFlags().Lookup(fakeFlag); f != nil { - _ = viper.BindPFlag(fakeFlag, f) - _ = viper.BindEnv(fakeFlag, "CRE_FAKE", "CL_FAKE") - } - - // Chain after whatever is already wired (notably the decode step RegisterCommandFlags just - // installed), so cfg is populated by the time these cross-flag rules are checked - and - // checked before any service starts, rather than at connect time. - prev := command.PersistentPreRunE - command.PersistentPreRunE = func(c *cobra.Command, args []string) error { - if prev != nil { - if err := prev(c, args); err != nil { - return err - } - } - // help and completion describe the program rather than run it, so they must work - // without a usable configuration - the library skips its own validation for them, and - // this check has to do the same. - if flags.IsBuiltinCommand(c) { - return nil - } - return d.cfg.validate(viper.GetBool(fakeFlag)) - } -} - var _ standalone.BootstrapDependency[*sql.DB] = (*dependency)(nil) diff --git a/libs/standalone/evm/dependency.go b/libs/standalone/evm/dependency.go index 3c3141383..76fae1a3c 100644 --- a/libs/standalone/evm/dependency.go +++ b/libs/standalone/evm/dependency.go @@ -6,10 +6,7 @@ import ( "math/big" "time" - "github.com/spf13/cobra" - "github.com/smartcontractkit/chainlink-common/pkg/config" - "github.com/smartcontractkit/chainlink-common/pkg/config/flags" "github.com/smartcontractkit/chainlink-common/pkg/logger" evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" @@ -47,7 +44,7 @@ const ( func Dependency(lggr logger.Logger) standalone.BootstrapDependency[evmclient.Client] { // Wrap in OnceBootstrapper so Get (which dials every configured RPC) runs at // most once even if several services resolve this dependency. - return standalone.OnceBootstrapper[evmclient.Client](&dependency{lggr: lggr}) + return standalone.OnceBootstrapper[evmclient.Client](&dependency{lggr: lggr, cfg: defaultConfig}) } // Config is the EVM client configuration. At least one http-url is required; WebSocket URLs @@ -82,13 +79,12 @@ var _ standalone.BootstrapDependency[evmclient.Client] = (*dependency)(nil) // Namespace groups the EVM settings under evm.* (--evm.http-url, CRE_EVM_HTTP_URL). func (d *dependency) Namespace() string { return "evm" } -func (d *dependency) AddCommands(cmd *cobra.Command) { - d.cfg = defaultConfig - opts := flags.DefaultTOMLOptions("CRE", "CL") - opts.Namespace = d.Namespace() - if err := flags.RegisterCommandFlags(cmd, &d.cfg, opts); err != nil { - panic(err) - } +func (d *dependency) Config() any { + return &d.cfg +} + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{} } func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmclient.Client, error) { diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl index cc0c9af00..1c7050071 100644 --- a/libs/standalone/gen/bootstrap.go.tmpl +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -20,11 +20,10 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( {{- end}} ) error { {{range RangeNum .}} - bootDep{{.}}.AddCommands(bs.root) dep{{.}} := &dependency[T{{.}}]{bd: bootDep{{.}}, bs: bs} {{end}} return bs.run(func(ctx context.Context) []services.Service { return fn(ctx, bs.config, {{range RangeNum .}}dep{{.}}, {{end}}) - }) + }, {{range RangeNum .}}bootDep{{.}},{{end}}) } {{- end }} diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index d733037ce..8468843ab 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -24,7 +24,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/prometheus/client_golang/prometheus" - "github.com/spf13/cobra" "github.com/smartcontractkit/libocr/networking" "github.com/smartcontractkit/libocr/networking/rageping" @@ -32,7 +31,6 @@ import ( ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/config" - "github.com/smartcontractkit/chainlink-common/pkg/config/flags" commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" commonocr "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" @@ -76,7 +74,7 @@ func (f *Factories) Close() error { func Dependency(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*Factories] { // Wrap in OnceBootstrapper so Get (which creates the peer or proxy clients) // runs at most once even if several services resolve this dependency. - return standalone.OnceBootstrapper[*Factories](&dependency{lggr: lggr, db: db, discovererTable: discovererTable}) + return standalone.OnceBootstrapper[*Factories](&dependency{lggr: lggr, db: db, discovererTable: discovererTable, cfg: defaultConfig}) } // Config is the libocr networking configuration. @@ -127,17 +125,12 @@ var _ standalone.BootstrapDependency[*Factories] = (*dependency)(nil) // CRE_OCR_LISTEN_ADDRESSES). func (d *dependency) Namespace() string { return "ocr" } -func (d *dependency) AddCommands(cmd *cobra.Command) { - // The database is a create/proxy-shared input (P2P identity, discoverer - // table), so surface its flags too - under its own namespace. - d.db.AddCommands(cmd) +func (d *dependency) Config() any { + return &d.cfg +} - d.cfg = defaultConfig - opts := flags.DefaultTOMLOptions("CRE", "CL") - opts.Namespace = d.Namespace() - if err := flags.RegisterCommandFlags(cmd, &d.cfg, opts); err != nil { - panic(err) - } +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{d.db} } func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Factories, error) { From 4ee0a920851ba21200e0b69b8442be37b076163f Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Mon, 10 Aug 2026 12:54:03 -0400 Subject: [PATCH 10/30] move config --- crecore/docs/CONFIG.md | 50 +++++----- libs/go.mod | 4 +- libs/standalone/db/db_dependency.go | 28 ++++-- libs/standalone/evm/dependency.go | 141 ++++------------------------ libs/standalone/ocr/dependency.go | 55 +++++------ 5 files changed, 92 insertions(+), 186 deletions(-) diff --git a/crecore/docs/CONFIG.md b/crecore/docs/CONFIG.md index b7ca4b273..fb24fddb0 100644 --- a/crecore/docs/CONFIG.md +++ b/crecore/docs/CONFIG.md @@ -7,8 +7,6 @@ proxy-listen-address = ':50051' capabilities-registry-address = '0xYourRegistryAddress' capabilities-registry-sync-interval = '12s' -[database] -url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' [ocr] listen-addresses = ['127.0.0.1:1234'] delta-reconcile = '1m0s' @@ -16,6 +14,8 @@ delta-dial = '5s' incoming-buffer-size = 100 outgoing-buffer-size = 100 keystore-password = 'xxxxx' +[database] +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' [evm] http-url = ['https://rpc.example.com'] chain-id = '1' @@ -61,25 +61,6 @@ fake = false # Docs only ``` fake use fake dependencies instead of real ones -## database -```toml -[database] -url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example -``` - - -### url -```toml -url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example -``` -url database url; required unless running with --fake and without --real-db - -### real-db -```toml -real-db = false # Docs only -``` -real-db use a real database even though --fake is set; requires --fake, and a url to point at - ## ocr ```toml [ocr] @@ -90,7 +71,7 @@ delta-dial = '5s' # Default incoming-buffer-size = 100 # Default outgoing-buffer-size = 100 # Default keystore-password = 'xxxxx' # Example -proxy-address = '' # Default +proxy-address = '' # Example ``` @@ -98,7 +79,7 @@ proxy-address = '' # Default ```toml listen-addresses = ['127.0.0.1:1234'] # Example ``` -listen-addresses rage p2p V2 listen addresses (host:port); creates a local peer (required unless proxy-address is set; must not be set when proxy-address is set) +listen-addresses rage p2p V2 listen addresses (host:port); creates a local peer ### announce-addresses ```toml @@ -138,9 +119,28 @@ keystore-password password for the node keystore holding the shared P2P identity ### proxy-address ```toml -proxy-address = '' # Default +proxy-address = '' # Example +``` +proxy-address delegate rage networking to a proxy at this gRPC address instead of creating a local peer (required unless listen-addresses is set; must not be set when listen-addresses is set) + +## database +```toml +[database] +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +``` + + +### url +```toml +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +``` +url database url + +### real-db +```toml +real-db = false # Docs only ``` -proxy-address delegate rage networking to a proxy at this gRPC address instead of creating a local peer (must not be set when listen-addresses is set) +real-db use a real database even though --fake is set; requires --fake, and a url to point at ## evm ```toml diff --git a/libs/go.mod b/libs/go.mod index 4a4cdb254..ae2944084 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -8,7 +8,6 @@ require ( github.com/google/uuid v1.6.0 github.com/grafana/pyroscope-go v1.3.0 github.com/hashicorp/go-plugin v1.8.0 - github.com/jackc/pgx/v5 v5.9.2 github.com/jmoiron/sqlx v1.4.0 github.com/pressly/goose/v3 v3.27.1 github.com/prometheus/client_golang v1.23.2 @@ -20,7 +19,6 @@ require ( github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 - github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 go.opentelemetry.io/otel v1.43.0 @@ -153,6 +151,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgtype v1.14.4 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jmhodges/levigo v1.0.0 // indirect @@ -223,6 +222,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/stellar/go-stellar-sdk v0.4.0 // indirect github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index 4882592d8..c289877c9 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -6,24 +6,28 @@ import ( "fmt" "io/fs" - // Register the pgx database/sql driver under the name "pgx". - _ "github.com/jackc/pgx/v5/stdlib" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/capabilities/libs/standalone" ) -// Config is the database configuration. Its two settings are related to --fake, which this -// struct does not own: +// Config is the database configuration: the connection settings that belong to the database +// itself (sqlutil.Config, which also knows how to open it), plus the one setting that only +// exists because this framework has a --fake mode. +// +// Both settings are related to --fake, which this struct does not own: // // - real-db only means something in fake mode, so it is rejected without --fake. -// - database-url is needed whenever a real database is actually used: always in normal mode, -// and in fake mode only when --real-db asks for one. +// - url is needed whenever a real database is actually used: always in normal mode, and in +// fake mode only when --real-db asks for one. // // Neither rule is expressible as a `validate` tag (both reference --fake), so neither shows up // in the generated docs on its own - the usage text spells them out so the docs still explain // when each setting applies. type Config struct { - URL string `toml:"url" usage:"database url; required unless running with --fake and without --real-db" example:"'postgresql://user:password@localhost:5432/chainlink?sslmode=disable'"` + // Embedded as a pointer so this struct adds to the shared settings rather than copying + // them; inline so its fields sit alongside real-db under database.* instead of nesting. + *sqlutil.Config `toml:",inline"` // Kept out of the example config: the example shows a normal run against a real database, // where this setting does not apply. It is still documented. @@ -31,7 +35,13 @@ type Config struct { } func Dependency(migrationsFS fs.FS, migrationTable string) standalone.BootstrapDependency[*sql.DB] { - return standalone.OnceBootstrapper[*sql.DB](&dependency{migrationsFS: migrationsFS, migrationTable: migrationTable}) + return standalone.OnceBootstrapper[*sql.DB](&dependency{ + migrationsFS: migrationsFS, + migrationTable: migrationTable, + // The embedded pointer is non-nil so the shared settings are bound and defaulted from + // here, the same way every other dependency starts from its config's defaults. + cfg: Config{Config: &sqlutil.Config{}}, + }) } type dependency struct { @@ -59,7 +69,7 @@ func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConf } var err error - d.db, err = sql.Open("pgx", d.cfg.URL) + d.db, err = sqlutil.OpenDB(*d.cfg.Config) if err != nil { return nil, err } diff --git a/libs/standalone/evm/dependency.go b/libs/standalone/evm/dependency.go index 76fae1a3c..8bbf662e8 100644 --- a/libs/standalone/evm/dependency.go +++ b/libs/standalone/evm/dependency.go @@ -2,76 +2,46 @@ package evm import ( "context" - "fmt" - "math/big" "time" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" - "github.com/smartcontractkit/chainlink-evm/pkg/config/chaintype" "github.com/smartcontractkit/capabilities/libs/standalone" ) -// Defaults mirror chainlink's own EVM chain defaults for a generic EVM chain, so -// a standalone binary polling the same RPCs behaves like the node does. +// Defaults for the settings evmclient.Config exposes, mirroring chainlink's own EVM chain +// defaults for a generic EVM chain, so a standalone binary polling the same RPCs behaves like +// the node does. They live here rather than in the client: this is the instance the flags are +// bound to and decoded into, so an unset setting keeps the value it was given here. const ( - defaultSelectionMode = "HighestHead" - defaultLeaseDuration = 0 - defaultPollFailureThreshold = uint32(5) - defaultPollSuccessThreshold = uint32(0) - defaultPollInterval = 10 * time.Second - defaultSyncThreshold = uint32(5) - defaultNoNewHeadsThreshold = 3 * time.Minute - defaultFinalityDepth = uint32(50) - defaultFinalizedBlockOffset = uint32(0) - defaultDeathDeclarationDelay = 10 * time.Second - defaultNoNewFinalizedHeads = 0 - defaultFinalizedBlockPollInterval = 5 * time.Second - defaultNewHeadsPollInterval = 0 - defaultConfirmationTimeout = 60 * time.Second - defaultSafeDepth = uint32(0) + defaultFinalityDepth = uint32(50) + defaultPollInterval = 10 * time.Second ) +var defaultConfig = evmclient.Config{ + FinalityTagEnabled: true, + FinalityDepth: defaultFinalityDepth, + PollInterval: *config.MustNewDuration(defaultPollInterval), +} + // Dependency returns a standalone.BootstrapDependency that resolves a dialed, -// multinode-backed EVM client. -// -// At least one --evm.http-url is required. WebSocket URLs are optional; without -// them the client polls for heads rather than subscribing, which is enough for -// view calls. +// multinode-backed EVM client. The settings and the dialing belong to the client +// itself (evmclient.Config); this binds them as bootstrap configuration under +// evm.*, defaults them, and resolves the client once. func Dependency(lggr logger.Logger) standalone.BootstrapDependency[evmclient.Client] { // Wrap in OnceBootstrapper so Get (which dials every configured RPC) runs at // most once even if several services resolve this dependency. return standalone.OnceBootstrapper[evmclient.Client](&dependency{lggr: lggr, cfg: defaultConfig}) } -// Config is the EVM client configuration. At least one http-url is required; WebSocket URLs -// are optional, and without them the client polls for heads rather than subscribing, which is -// enough for view calls. -type Config struct { - HTTPURLs []string `toml:"http-url" usage:"EVM RPC HTTP URL(s); repeat or comma-separate for a multinode pool" validate:"required" example:"['https://rpc.example.com']"` - WSURLs []string `toml:"ws-url" usage:"EVM RPC WebSocket URL(s), positionally paired with --evm.http-url; optional" validate:"excluded_without=HTTPURLs"` - ChainID string `toml:"chain-id" usage:"EVM chain ID" validate:"required" example:"'1'"` - ChainType string `toml:"chain-type" usage:"EVM chain type (empty for a generic EVM chain)"` - - FinalityTagEnabled bool `toml:"finality-tag-enabled" usage:"use the finalized block tag instead of a finality depth"` - FinalityDepth uint32 `toml:"finality-depth" usage:"finality depth, used when --evm.finality-tag-enabled=false"` - PollInterval config.Duration `toml:"poll-interval" usage:"per-node health poll interval"` -} - -var defaultConfig = Config{ - FinalityTagEnabled: true, - FinalityDepth: defaultFinalityDepth, - PollInterval: *config.MustNewDuration(defaultPollInterval), -} - type dependency struct { lggr logger.Logger client evmclient.Client - cfg Config + cfg evmclient.Config } var _ standalone.BootstrapDependency[evmclient.Client] = (*dependency)(nil) @@ -88,85 +58,10 @@ func (d *dependency) Dependencies() []standalone.BootstrapCommand { } func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmclient.Client, error) { - if len(d.cfg.WSURLs) > 0 && len(d.cfg.WSURLs) != len(d.cfg.HTTPURLs) { - return nil, fmt.Errorf("--evm.ws-url count (%d) must match --evm.http-url count (%d) when provided", len(d.cfg.WSURLs), len(d.cfg.HTTPURLs)) - } - - chainID, ok := new(big.Int).SetString(d.cfg.ChainID, 10) - if !ok { - return nil, fmt.Errorf("invalid --evm.chain-id %q", d.cfg.ChainID) - } - - nodeCfgs := make([]evmclient.NodeConfig, len(d.cfg.HTTPURLs)) - for i := range d.cfg.HTTPURLs { - name := fmt.Sprintf("node-%d", i) - order := int32(1) - sendOnly := false - loadBalanced := false - cfg := evmclient.NodeConfig{ - Name: &name, - HTTPURL: &d.cfg.HTTPURLs[i], - Order: &order, - SendOnly: &sendOnly, - IsLoadBalancedRPC: &loadBalanced, - } - if len(d.cfg.WSURLs) > 0 { - cfg.WSURL = &d.cfg.WSURLs[i] - } - nodeCfgs[i] = cfg - } - - selectionMode := defaultSelectionMode - pollFailureThreshold := defaultPollFailureThreshold - pollSuccessThreshold := defaultPollSuccessThreshold - syncThreshold := defaultSyncThreshold - nodeIsSyncingEnabled := false - finalityDepth := d.cfg.FinalityDepth - finalityTagEnabled := d.cfg.FinalityTagEnabled - safeTagSupported := false - finalizedBlockOffset := defaultFinalizedBlockOffset - enforceRepeatableRead := true - safeDepth := defaultSafeDepth - - chainCfg, nodePool, nodes, err := evmclient.NewClientConfigs( - &selectionMode, - defaultLeaseDuration, - d.cfg.ChainType, - nodeCfgs, - &pollFailureThreshold, - &pollSuccessThreshold, - d.cfg.PollInterval.Duration(), - &syncThreshold, - &nodeIsSyncingEnabled, - defaultNoNewHeadsThreshold, - &finalityDepth, - &finalityTagEnabled, - &safeTagSupported, - &finalizedBlockOffset, - &enforceRepeatableRead, - defaultDeathDeclarationDelay, - defaultNoNewFinalizedHeads, - defaultFinalizedBlockPollInterval, - defaultNewHeadsPollInterval, - defaultConfirmationTimeout, - &safeDepth, - ) + cl, err := evmclient.NewClientFromConfig(ctx, d.lggr, d.cfg) if err != nil { - return nil, fmt.Errorf("failed to build evm client configs: %w", err) + return nil, err } - - // clientErrors is nil: it only feeds ClassifySendError on the transaction send - // path, and this client is read-only. - cl, err := evmclient.NewEvmClient(nodePool, chainCfg, nil, d.lggr, chainID, nodes, chaintype.ChainType(d.cfg.ChainType)) - if err != nil { - return nil, fmt.Errorf("failed to create evm client: %w", err) - } - - if err := cl.Dial(ctx); err != nil { - return nil, fmt.Errorf("failed to dial evm client: %w", err) - } - - d.lggr.Infow("EVM client dialed", "chainID", chainID, "nodes", len(nodes), "selectionMode", selectionMode) d.client = cl return cl, nil } diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index 8468843ab..f10a2d619 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -11,8 +11,9 @@ // --ocr.proxy-address, exposing proxy-client-backed factories instead of a // local peer. // -// The two modes are wired as a cobra "one of" set: exactly one of -// --ocr.listen-addresses / --ocr.proxy-address may (and must) be provided. +// Exactly one of --ocr.listen-addresses / --ocr.proxy-address may (and must) be +// provided; the peer's own settings come from chainlink-common's ocrcommon.Config, +// which this package's Config embeds and adds proxy mode to. package ocr import ( @@ -32,7 +33,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/config" commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" - commonocr "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" + "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" "github.com/smartcontractkit/capabilities/libs/standalone" @@ -74,41 +75,41 @@ func (f *Factories) Close() error { func Dependency(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*Factories] { // Wrap in OnceBootstrapper so Get (which creates the peer or proxy clients) // runs at most once even if several services resolve this dependency. - return standalone.OnceBootstrapper[*Factories](&dependency{lggr: lggr, db: db, discovererTable: discovererTable, cfg: defaultConfig}) + return standalone.OnceBootstrapper[*Factories](&dependency{lggr: lggr, db: db, discovererTable: discovererTable, cfg: defaultConfig()}) } -// Config is the libocr networking configuration. +// Config is the libocr networking configuration: the settings of the local peer itself +// (ocrcommon.Config, shared with anything else that runs one), plus proxy mode, which is this +// framework's own alternative to running that peer. // // The two modes are expressed as validator tags rather than cobra's // MarkFlagsMutuallyExclusive/MarkFlagsOneRequired: those only inspect whether a flag was // literally typed on the command line, so they would reject a mode that was selected via a // config file or env var. required_without/excluded_with are checked against the decoded -// values instead, so exactly-one-of holds no matter which source supplied it. +// values instead, so exactly-one-of holds no matter which source supplied it. Both rules sit on +// ProxyAddress, the field this struct owns: a rule on the embedded ListenAddresses could not +// name a field outside its own struct. type Config struct { - // create-mode config - ListenAddresses []string `toml:"listen-addresses" usage:"rage p2p V2 listen addresses (host:port); creates a local peer" validate:"required_without=ProxyAddress,excluded_with=ProxyAddress" example:"['127.0.0.1:1234']"` - AnnounceAddresses []string `toml:"announce-addresses" usage:"rage p2p V2 announce addresses (host:port); defaults to the listen addresses" validate:"excluded_without=ListenAddresses"` - DeltaReconcile config.Duration `toml:"delta-reconcile" usage:"rage p2p V2 delta reconcile interval"` - DeltaDial config.Duration `toml:"delta-dial" usage:"rage p2p V2 minimum interval between dial attempts"` - - IncomingBufferSize int `toml:"incoming-buffer-size" usage:"per-remote incoming message buffer size"` - OutgoingBufferSize int `toml:"outgoing-buffer-size" usage:"per-remote outgoing message buffer size"` - - // KeystorePassword unlocks the node's key ring, which is where the shared P2P identity - // comes from. Both modes need it: the peer ID is what other DON members expect at this - // address, whether this process runs the peer itself or delegates to a proxy. Typed as a - // SecretString so it redacts itself in logs, docs and the example config. - KeystorePassword config.SecretString `toml:"keystore-password" usage:"password for the node keystore holding the shared P2P identity" validate:"required"` + // Embedded as a pointer so this struct adds proxy mode to the shared peer settings rather + // than copying them; inline so its fields sit alongside proxy-address under ocr.* instead + // of nesting. + *ocrcommon.Config `toml:",inline"` // proxy-mode config - ProxyAddress string `toml:"proxy-address" usage:"delegate rage networking to a proxy at this gRPC address instead of creating a local peer" validate:"excluded_with=ListenAddresses"` + ProxyAddress string `toml:"proxy-address" usage:"delegate rage networking to a proxy at this gRPC address instead of creating a local peer" validate:"required_without=ListenAddresses,excluded_with=ListenAddresses"` } -var defaultConfig = Config{ - DeltaReconcile: *config.MustNewDuration(time.Minute), - DeltaDial: *config.MustNewDuration(5 * time.Second), - IncomingBufferSize: 100, - OutgoingBufferSize: 100, +// defaultConfig is the instance the flags are bound to and decoded into, so an unset setting +// keeps the value it is given here. The embedded pointer is fresh per call rather than a shared +// package-level value, so two dependencies can never decode into the same peer settings. Proxy +// mode has no default: an unset proxy address is what selects a local peer. +func defaultConfig() Config { + return Config{Config: &ocrcommon.Config{ + DeltaReconcile: *config.MustNewDuration(time.Minute), + DeltaDial: *config.MustNewDuration(5 * time.Second), + IncomingBufferSize: 100, + OutgoingBufferSize: 100, + }} } type dependency struct { @@ -156,7 +157,7 @@ func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Fact // localFactories builds a real libocr peer and exposes its factories. func (d *dependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*Factories, error) { - discovererDB := commonocr.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) + discovererDB := ocrcommon.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) d.lggr.Infow("Creating local p2p peer", "peerID", peerID.String(), From f9a91779ed96f5059c729d7338e4bd8af5679521 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Mon, 10 Aug 2026 14:23:25 -0400 Subject: [PATCH 11/30] Proper names --- crecore/main.go | 2 +- crecore/proxy_service.go | 14 +++++++------- libs/standalone/bootstrapper.go | 2 +- libs/standalone/db/db_dependency.go | 4 ++-- libs/standalone/ocr/dependency.go | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crecore/main.go b/crecore/main.go index c73e4888f..cb042df43 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -77,7 +77,7 @@ run "docs" to write the full reference to docs/CONFIG.md.`, factories standalone.Dependency[*ocr.Factories], evmClient standalone.Dependency[evmclient.Client], ) []services.Service { - regSvc := newRegistryService(cfg.RegistryAddress, cfg.RegistrySyncInterval.Duration(), + regSvc := newRegistryService(cfg.CapabilitiesRegistryAddress, cfg.CapabilitiesRegistrySyncInterval.Duration(), scfg.Logger.Named("capabilities registry"), evmClient, factories) // The registry attaches to the proxy's gRPC server, so core reaches both // over the single --proxy-listen-address it already configures. diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index 181d7b77f..f2d73dadd 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -23,19 +23,19 @@ import ( // instead. type Config struct { // ProxyListenAddress is the address the proxy gRPC server listens on. - ProxyListenAddress string `toml:"proxy-listen-address" usage:"address the proxy gRPC server listens on"` + ProxyListenAddress string `usage:"address the proxy gRPC server listens on"` - // RegistryAddress is the on-chain CapabilitiesRegistry (v2) contract address. The registry + // CapabilitiesRegistryAddress is the on-chain CapabilitiesRegistry (v2) contract address. The registry // always runs, so this is as required in practice as the registry itself. - RegistryAddress string `toml:"capabilities-registry-address" usage:"on-chain CapabilitiesRegistry (v2) contract address" validate:"required" example:"'0xYourRegistryAddress'"` + CapabilitiesRegistryAddress string `usage:"on-chain CapabilitiesRegistry (v2) contract address" validate:"required" example:"'0xYourRegistryAddress'"` - // RegistrySyncInterval is how often the on-chain registry is re-read. - RegistrySyncInterval config.Duration `toml:"capabilities-registry-sync-interval" usage:"how often the on-chain registry is re-read"` + // CapabilitiesRegistrySyncInterval is how often the on-chain registry is re-read. + CapabilitiesRegistrySyncInterval config.Duration `usage:"how often the on-chain registry is re-read"` } var defaultConfig = Config{ - ProxyListenAddress: ":50051", - RegistrySyncInterval: *config.MustNewDuration(registry.DefaultSyncInterval), + ProxyListenAddress: ":50051", + CapabilitiesRegistrySyncInterval: *config.MustNewDuration(registry.DefaultSyncInterval), } // proxyService exposes the libocr rage networking factories over gRPC so that diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index d42a656d1..664d73b52 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -280,7 +280,7 @@ func underPluginHost() bool { type CommonConfig struct { // Kept out of the example config: the example shows a real run, and --fake selects a // different set of dependencies than the ones it illustrates. - Fake bool `toml:"fake" usage:"use fake dependencies instead of real ones" flagdocs:"noexample"` + Fake bool `usage:"use fake dependencies instead of real ones" flagdocs:"noexample"` } type Dependency[T any] interface { diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index c289877c9..9cd3fce60 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -31,7 +31,7 @@ type Config struct { // Kept out of the example config: the example shows a normal run against a real database, // where this setting does not apply. It is still documented. - UseRealDBForFake bool `toml:"real-db" usage:"use a real database even though --fake is set; requires --fake, and a url to point at" flagdocs:"noexample"` + RealDB bool `usage:"use a real database even though --fake is set; requires --fake, and a url to point at" flagdocs:"noexample"` } func Dependency(migrationsFS fs.FS, migrationTable string) standalone.BootstrapDependency[*sql.DB] { @@ -60,7 +60,7 @@ func (d *dependency) Dependencies() []standalone.BootstrapCommand { } func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConfig) (*sql.DB, error) { - if commonConfig.Fake && !d.cfg.UseRealDBForFake { + if commonConfig.Fake && !d.cfg.RealDB { if d.cfg.URL != "" { return nil, fmt.Errorf("database url set when using fake database") } diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index f10a2d619..54f4546fd 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -96,7 +96,7 @@ type Config struct { *ocrcommon.Config `toml:",inline"` // proxy-mode config - ProxyAddress string `toml:"proxy-address" usage:"delegate rage networking to a proxy at this gRPC address instead of creating a local peer" validate:"required_without=ListenAddresses,excluded_with=ListenAddresses"` + ProxyAddress string `usage:"delegate rage networking to a proxy at this gRPC address instead of creating a local peer" validate:"required_without=ListenAddresses,excluded_with=ListenAddresses"` } // defaultConfig is the instance the flags are bound to and decoded into, so an unset setting From 840ed36acd3bfe24765c49b1b568e75417f06e0b Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Tue, 11 Aug 2026 11:20:41 -0400 Subject: [PATCH 12/30] Small fixes for what's where and attempt to introduce embedded (untested) --- crecore/docs/CONFIG.md | 293 +++++++++--- crecore/main.go | 28 +- crecore/proxy_service.go | 28 +- libs/standalone/bootstrap_gen.go | 310 ++++++++---- libs/standalone/bootstrapper.go | 502 +++++++++++++++----- libs/standalone/cmd_helpers.go | 19 + libs/standalone/config.go | 157 ++++++ libs/standalone/config_test.go | 70 +++ libs/standalone/db/db_dependency.go | 148 ++++-- libs/standalone/db/db_dependency_test.go | 63 +++ libs/standalone/evm/dependency.go | 5 + libs/standalone/gen/bootstrap.go.tmpl | 25 +- libs/standalone/listener/dependency.go | 108 +++++ libs/standalone/listener/dependency_test.go | 71 +++ libs/standalone/ocr/dependency.go | 213 ++------- libs/standalone/ocr/embed.go | 74 +++ libs/standalone/ocr/host.go | 161 +++++++ libs/standalone/ocr/host_test.go | 45 ++ libs/standalone/ocr/inproc.go | 485 +++++++++++++++++++ libs/standalone/ocr/inproc_test.go | 255 ++++++++++ libs/standalone/ocr/keyring.go | 45 +- libs/standalone/ocr/keyring_test.go | 58 +++ libs/standalone/ocr/proxy.go | 115 +++++ libs/standalone/ocr/proxy_test.go | 65 +++ libs/standalone/ports.go | 46 ++ libs/standalone/ports_test.go | 58 +++ libs/standalone/pyroscope.go | 29 +- libs/standalone/telemetry.go | 154 +++--- libs/standalone/webserver.go | 103 ++++ 29 files changed, 3088 insertions(+), 645 deletions(-) create mode 100644 libs/standalone/config.go create mode 100644 libs/standalone/config_test.go create mode 100644 libs/standalone/db/db_dependency_test.go create mode 100644 libs/standalone/listener/dependency.go create mode 100644 libs/standalone/listener/dependency_test.go create mode 100644 libs/standalone/ocr/embed.go create mode 100644 libs/standalone/ocr/host.go create mode 100644 libs/standalone/ocr/host_test.go create mode 100644 libs/standalone/ocr/inproc.go create mode 100644 libs/standalone/ocr/inproc_test.go create mode 100644 libs/standalone/ocr/keyring_test.go create mode 100644 libs/standalone/ocr/proxy.go create mode 100644 libs/standalone/ocr/proxy_test.go create mode 100644 libs/standalone/ports.go create mode 100644 libs/standalone/ports_test.go create mode 100644 libs/standalone/webserver.go diff --git a/crecore/docs/CONFIG.md b/crecore/docs/CONFIG.md index fb24fddb0..8eeb2f264 100644 --- a/crecore/docs/CONFIG.md +++ b/crecore/docs/CONFIG.md @@ -4,9 +4,42 @@ ```toml # ----- Global Configuration ----- -proxy-listen-address = ':50051' capabilities-registry-address = '0xYourRegistryAddress' capabilities-registry-sync-interval = '12s' +[telemetry] +endpoint = '' +insecure-connection = false +ca-cert-file = '' +attributes = ['env=staging'] +auth-pub-key-hex = '' +auth-headers-ttl = '0s' +prometheus-bridge-enabled = false +[tracing] +enabled = false +sampling-ratio = 1 +tls-cert-file = '' +[chip-ingress] +endpoint = '' +insecure-connection = false +[pyroscope] +server-address = '' +environment = '' +[prometheus] +port = -1 +[evm] +http-url = ['https://rpc.example.com'] +chain-id = '1' +chain-type = '' +finality-tag-enabled = true +finality-depth = 50 +poll-interval = '10s' +[proxy] +listen-address = ':50051' + +# ----- Command: main embed ----- +instances = 1 + +# ----- Command: main run ----- [ocr] listen-addresses = ['127.0.0.1:1234'] delta-reconcile = '1m0s' @@ -16,20 +49,12 @@ outgoing-buffer-size = 100 keystore-password = 'xxxxx' [database] url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' -[evm] -http-url = ['https://rpc.example.com'] -chain-id = '1' -chain-type = '' -finality-tag-enabled = true -finality-depth = 50 -poll-interval = '10s' ``` ## Global ```toml -proxy-listen-address = ':50051' # Default capabilities-registry-address = '0xYourRegistryAddress' # Example capabilities-registry-sync-interval = '12s' # Default ``` @@ -37,12 +62,6 @@ capabilities-registry-sync-interval = '12s' # Default # Global Configuration -### proxy-listen-address -```toml -proxy-listen-address = ':50051' # Default -``` -proxy-listen-address address the proxy gRPC server listens on - ### capabilities-registry-address ```toml capabilities-registry-address = '0xYourRegistryAddress' # Example @@ -55,92 +74,152 @@ capabilities-registry-sync-interval = '12s' # Default ``` capabilities-registry-sync-interval how often the on-chain registry is re-read -### fake +## telemetry ```toml -fake = false # Docs only +[telemetry] +endpoint = '' # Default +insecure-connection = false # Default +ca-cert-file = '' # Default +attributes = ['env=staging'] # Example +auth-pub-key-hex = '' # Default +auth-headers-ttl = '0s' # Default +prometheus-bridge-enabled = false # Default ``` -fake use fake dependencies instead of real ones -## ocr + +### endpoint ```toml -[ocr] -listen-addresses = ['127.0.0.1:1234'] # Example -announce-addresses = [] # Default -delta-reconcile = '1m0s' # Default -delta-dial = '5s' # Default -incoming-buffer-size = 100 # Default -outgoing-buffer-size = 100 # Default -keystore-password = 'xxxxx' # Example -proxy-address = '' # Example +endpoint = '' # Default ``` +endpoint OTLP gRPC endpoint telemetry is exported to; telemetry is disabled when unset +### insecure-connection +```toml +insecure-connection = false # Default +``` +insecure-connection export telemetry over an insecure connection -### listen-addresses +### ca-cert-file ```toml -listen-addresses = ['127.0.0.1:1234'] # Example +ca-cert-file = '' # Default ``` -listen-addresses rage p2p V2 listen addresses (host:port); creates a local peer +ca-cert-file CA certificate file used to verify the telemetry endpoint -### announce-addresses +### attributes ```toml -announce-addresses = [] # Default +attributes = ['env=staging'] # Example ``` -announce-addresses rage p2p V2 announce addresses (host:port); defaults to the listen addresses (must not be set unless listen-addresses is set) +attributes extra telemetry resource attributes, as key=value pairs -### delta-reconcile +### auth-headers ```toml -delta-reconcile = '1m0s' # Default +auth-headers = [] # Docs only ``` -delta-reconcile rage p2p V2 delta reconcile interval +auth-headers telemetry auth headers, as key=value pairs -### delta-dial +### auth-pub-key-hex ```toml -delta-dial = '5s' # Default +auth-pub-key-hex = '' # Default ``` -delta-dial rage p2p V2 minimum interval between dial attempts +auth-pub-key-hex public key the telemetry auth headers are derived from -### incoming-buffer-size +### auth-headers-ttl ```toml -incoming-buffer-size = 100 # Default +auth-headers-ttl = '0s' # Default ``` -incoming-buffer-size per-remote incoming message buffer size +auth-headers-ttl how long generated telemetry auth headers are valid for -### outgoing-buffer-size +### prometheus-bridge-enabled ```toml -outgoing-buffer-size = 100 # Default +prometheus-bridge-enabled = false # Default ``` -outgoing-buffer-size per-remote outgoing message buffer size +prometheus-bridge-enabled feed metrics registered on the prometheus registry into the telemetry pipeline -### keystore-password +## tracing ```toml -keystore-password = 'xxxxx' # Example +[tracing] +enabled = false # Default +sampling-ratio = 1 # Default +tls-cert-file = '' # Default ``` -keystore-password password for the node keystore holding the shared P2P identity -### proxy-address + +### enabled ```toml -proxy-address = '' # Example +enabled = false # Default ``` -proxy-address delegate rage networking to a proxy at this gRPC address instead of creating a local peer (required unless listen-addresses is set; must not be set when listen-addresses is set) +enabled export traces to the telemetry endpoint -## database +### sampling-ratio ```toml -[database] -url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +sampling-ratio = 1 # Default ``` +sampling-ratio fraction of traces sampled, from 0 to 1 +### tls-cert-file +```toml +tls-cert-file = '' # Default +``` +tls-cert-file TLS certificate file used by the trace exporter -### url +## chip-ingress ```toml -url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +[chip-ingress] +endpoint = '' # Default +insecure-connection = false # Default ``` -url database url -### real-db + +### endpoint +```toml +endpoint = '' # Default +``` +endpoint chip ingress gRPC endpoint; the emitter is disabled when unset + +### insecure-connection +```toml +insecure-connection = false # Default +``` +insecure-connection connect to chip ingress over an insecure connection + +## pyroscope +```toml +[pyroscope] +server-address = '' # Default +environment = '' # Default +``` + + +### server-address +```toml +server-address = '' # Default +``` +server-address pyroscope server address; profiling is disabled when unset + +### auth-token +```toml +auth-token = 'xxxxx' # Docs only +``` +auth-token pyroscope auth token + +### environment ```toml -real-db = false # Docs only +environment = '' # Default ``` -real-db use a real database even though --fake is set; requires --fake, and a url to point at +environment tag attached to profiles + +## prometheus +```toml +[prometheus] +port = -1 # Default +``` + + +### port +```toml +port = -1 # Default +``` +port serving /metrics, /debug/pprof, /healthz and /readyz; -1 disables the server, 0 asks the OS for an ephemeral port. Instance i of an embed run listens on this port plus i ## evm ```toml @@ -197,3 +276,95 @@ poll-interval = '10s' # Default ``` poll-interval per-node health poll interval +## proxy +```toml +[proxy] +listen-address = ':50051' # Default +instances = 1 # Default +``` + + +### listen-address +```toml +listen-address = ':50051' # Default +``` +listen-address address (host:port) this server listens on; instance i of an embed run listens on the port plus i + +# Command: main embed + +### instances +```toml +instances = 1 # Default +``` +instances number of instances to run in this process + +# Command: main run + +## ocr +```toml +[ocr] +listen-addresses = ['127.0.0.1:1234'] # Example +announce-addresses = [] # Default +delta-reconcile = '1m0s' # Default +delta-dial = '5s' # Default +incoming-buffer-size = 100 # Default +outgoing-buffer-size = 100 # Default +keystore-password = 'xxxxx' # Default +``` + + +### listen-addresses +```toml +listen-addresses = ['127.0.0.1:1234'] # Example +``` +listen-addresses rage p2p V2 listen addresses (host:port); creates a local peer + +### announce-addresses +```toml +announce-addresses = [] # Default +``` +announce-addresses rage p2p V2 announce addresses (host:port); defaults to the listen addresses (must not be set unless listen-addresses is set) + +### delta-reconcile +```toml +delta-reconcile = '1m0s' # Default +``` +delta-reconcile rage p2p V2 delta reconcile interval + +### delta-dial +```toml +delta-dial = '5s' # Default +``` +delta-dial rage p2p V2 minimum interval between dial attempts + +### incoming-buffer-size +```toml +incoming-buffer-size = 100 # Default +``` +incoming-buffer-size per-remote incoming message buffer size + +### outgoing-buffer-size +```toml +outgoing-buffer-size = 100 # Default +``` +outgoing-buffer-size per-remote outgoing message buffer size + +### keystore-password +```toml +keystore-password = 'xxxxx' # Default +``` +keystore-password password for the node keystore holding the shared P2P identity; required unless the identity is derived, as it is under embed + +## database +```toml +[database] +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +``` + + +### url +```toml +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' # Example +``` +url database url + diff --git a/crecore/main.go b/crecore/main.go index cb042df43..00f0dcff8 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -4,12 +4,14 @@ import ( "context" "embed" "log" + "net" "github.com/spf13/cobra" "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/db" "github.com/smartcontractkit/capabilities/libs/standalone/evm" + "github.com/smartcontractkit/capabilities/libs/standalone/listener" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" "github.com/smartcontractkit/chainlink-common/pkg/config/flags" @@ -26,6 +28,9 @@ const migrationsTable = "proxy_migrations" // CREATE TABLE in migrations/0001_*.sql. const ocrDiscovererTable = "proxy_ocr_discoverer_announcements" +// defaultListenAddress is where the proxy and registry are served when no address is configured. +const defaultListenAddress = ":50051" + func main() { if err := run(); err != nil { log.Fatal(err) @@ -61,27 +66,32 @@ run "docs" to write the full reference to docs/CONFIG.md.`, bootstrapper := standalone.NewBootstrapper(root, standalone.WithOtelViews(metricViews())) lggr := bootstrapper.Logger() - // The ocr dependency owns the libocr networking config (create vs proxy - // mode, and the keystore password that unlocks the peer identity) and wraps - // the database dependency it needs for that identity and the OCR discoverer - // table. + // This binary is the process that hosts the peer, so it takes the ocr host dependency rather + // than the proxy one: it owns the libocr networking config (the peer's own settings, and the + // keystore password that unlocks its identity) and wraps the database dependency it needs for + // that identity and the OCR discoverer table. dbDep := db.Dependency(embeddedMigrations, migrationsTable) - ocrDep := ocr.Dependency(lggr.Named("OCR"), dbDep, ocrDiscovererTable) + ocrDep := ocr.Host(lggr.Named("OCR"), dbDep, ocrDiscovererTable) // The registry always runs, so the EVM client is always resolved and the // evm settings are as required in practice as the registry address is. evmDep := evm.Dependency(lggr.Named("EVM")) + // Where this process serves, as a dependency rather than a setting the proxy service reads: + // the address is the one thing two instances in one process cannot agree on, and resolving it + // per instance keeps that entirely out of the service. + listenerDep := listener.Dependency("proxy", defaultListenAddress) - return standalone.Run2(bootstrapper, func( + return standalone.Run3(bootstrapper, func( ctx context.Context, scfg *standalone.StandaloneConfig, factories standalone.Dependency[*ocr.Factories], evmClient standalone.Dependency[evmclient.Client], + lis standalone.Dependency[net.Listener], ) []services.Service { regSvc := newRegistryService(cfg.CapabilitiesRegistryAddress, cfg.CapabilitiesRegistrySyncInterval.Duration(), scfg.Logger.Named("capabilities registry"), evmClient, factories) // The registry attaches to the proxy's gRPC server, so core reaches both - // over the single --proxy-listen-address it already configures. - proxySvc := newProxyService(&cfg, scfg.Logger.Named("proxy service"), factories, regSvc.Register) + // over the single address it already configures. + proxySvc := newProxyService(scfg.Logger.Named("proxy service"), lis, factories, regSvc.Register) return []services.Service{proxySvc, regSvc} - }, ocrDep, evmDep) + }, ocrDep, evmDep, listenerDep) } diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index f2d73dadd..efe5efe92 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "net" @@ -19,12 +18,9 @@ import ( ) // Config is the root command's configuration, populated by flags.RegisterCommandFlags (see -// main.go). The libocr peer / proxy-client configuration lives on the ocr bootstrap dependency -// instead. +// main.go). The libocr peer configuration lives on the ocr bootstrap dependency, and the address +// this process serves on lives on the listener dependency. type Config struct { - // ProxyListenAddress is the address the proxy gRPC server listens on. - ProxyListenAddress string `usage:"address the proxy gRPC server listens on"` - // CapabilitiesRegistryAddress is the on-chain CapabilitiesRegistry (v2) contract address. The registry // always runs, so this is as required in practice as the registry itself. CapabilitiesRegistryAddress string `usage:"on-chain CapabilitiesRegistry (v2) contract address" validate:"required" example:"'0xYourRegistryAddress'"` @@ -34,7 +30,6 @@ type Config struct { } var defaultConfig = Config{ - ProxyListenAddress: ":50051", CapabilitiesRegistrySyncInterval: *config.MustNewDuration(registry.DefaultSyncInterval), } @@ -49,8 +44,11 @@ type proxyService struct { services.Service eng *services.Engine - cfg *Config - lggr logger.Logger + lggr logger.Logger + // listener is where this process serves. Resolved as a dependency rather than opened from a + // configured address, so a process running several instances gives each of them a socket of + // its own without this service knowing that more than one exists. + listener standalone.Dependency[net.Listener] factories standalone.Dependency[*ocr.Factories] // registrars attach additional gRPC services to this server before it @@ -67,8 +65,8 @@ var _ services.Service = (*proxyService)(nil) // newProxyService builds the proxy service using the standard // services.Config/Engine pattern, so its lifecycle and health integrate with // the bootstrapper's aggregated health report. -func newProxyService(cfg *Config, lggr logger.Logger, factories standalone.Dependency[*ocr.Factories], registrars ...func(*grpc.Server)) *proxyService { - s := &proxyService{cfg: cfg, lggr: lggr, factories: factories, registrars: registrars} +func newProxyService(lggr logger.Logger, listener standalone.Dependency[net.Listener], factories standalone.Dependency[*ocr.Factories], registrars ...func(*grpc.Server)) *proxyService { + s := &proxyService{lggr: lggr, listener: listener, factories: factories, registrars: registrars} s.Service, s.eng = services.Config{ Name: "P2PProxy", Start: s.start, @@ -78,10 +76,6 @@ func newProxyService(cfg *Config, lggr logger.Logger, factories standalone.Depen } func (s *proxyService) start(ctx context.Context) error { - if s.cfg.ProxyListenAddress == "" { - return errors.New("--proxy-listen-address is required") - } - factories, err := s.factories.Get(ctx) if err != nil { return fmt.Errorf("failed to get libocr factories: %w", err) @@ -93,9 +87,9 @@ func (s *proxyService) start(ctx context.Context) error { return fmt.Errorf("failed to create proxy metrics: %w", err) } - lis, err := net.Listen("tcp", s.cfg.ProxyListenAddress) + lis, err := s.listener.Get(ctx) if err != nil { - return fmt.Errorf("failed to listen on %s: %w", s.cfg.ProxyListenAddress, err) + return fmt.Errorf("failed to get listener: %w", err) } // The factories back both surfaces over the same rage connection and diff --git a/libs/standalone/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go index a2124e24f..7ad8ff70b 100644 --- a/libs/standalone/bootstrap_gen.go +++ b/libs/standalone/bootstrap_gen.go @@ -12,43 +12,74 @@ import ( // fn receives the context, the StandaloneConfig, and the 1 dependency, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run1[T0 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0]) []services.Service, bootDep0 BootstrapDependency[T0], ) error { - - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0) - }, bootDep0) + return bs.run(func(index int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, embed) + + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0) + } + }, + []BootstrapCommand{bootDep0}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0)}, + ) } // Run2 bootstraps the services built from 2 dependencies. // fn receives the context, the StandaloneConfig, and the 2 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run2[T0 any, T1 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1]) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], ) error { - - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} - - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} - - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1) - }, bootDep0, bootDep1) + return bs.run(func(index int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, embed) + + dep1 := instanceOf(bs, bootDep1, index, embed) + + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1) + } + }, + []BootstrapCommand{bootDep0, bootDep1}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0)}, + ) } // Run3 bootstraps the services built from 3 dependencies. // fn receives the context, the StandaloneConfig, and the 3 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run3[T0 any, T1 any, T2 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2]) []services.Service, @@ -56,22 +87,35 @@ func Run3[T0 any, T1 any, T2 any]( bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2) - }, bootDep0, bootDep1, bootDep2) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0)}, + ) } // Run4 bootstraps the services built from 4 dependencies. // fn receives the context, the StandaloneConfig, and the 4 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run4[T0 any, T1 any, T2 any, T3 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3]) []services.Service, @@ -80,24 +124,37 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( bootDep2 BootstrapDependency[T2], bootDep3 BootstrapDependency[T3], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + dep3 := instanceOf(bs, bootDep3, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2, dep3) - }, bootDep0, bootDep1, bootDep2, bootDep3) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2, dep3) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0)}, + ) } // Run5 bootstraps the services built from 5 dependencies. // fn receives the context, the StandaloneConfig, and the 5 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4]) []services.Service, @@ -107,26 +164,39 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( bootDep3 BootstrapDependency[T3], bootDep4 BootstrapDependency[T4], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + dep3 := instanceOf(bs, bootDep3, index, embed) - dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + dep4 := instanceOf(bs, bootDep4, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4) - }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0)}, + ) } // Run6 bootstraps the services built from 6 dependencies. // fn receives the context, the StandaloneConfig, and the 6 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5]) []services.Service, @@ -137,28 +207,41 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( bootDep4 BootstrapDependency[T4], bootDep5 BootstrapDependency[T5], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + dep3 := instanceOf(bs, bootDep3, index, embed) - dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + dep4 := instanceOf(bs, bootDep4, index, embed) - dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + dep5 := instanceOf(bs, bootDep5, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5) - }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0)}, + ) } // Run7 bootstraps the services built from 7 dependencies. // fn receives the context, the StandaloneConfig, and the 7 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6]) []services.Service, @@ -170,30 +253,43 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( bootDep5 BootstrapDependency[T5], bootDep6 BootstrapDependency[T6], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + dep3 := instanceOf(bs, bootDep3, index, embed) - dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + dep4 := instanceOf(bs, bootDep4, index, embed) - dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + dep5 := instanceOf(bs, bootDep5, index, embed) - dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + dep6 := instanceOf(bs, bootDep6, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6) - }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0)}, + ) } // Run8 bootstraps the services built from 8 dependencies. // fn receives the context, the StandaloneConfig, and the 8 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7]) []services.Service, @@ -206,32 +302,45 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( bootDep6 BootstrapDependency[T6], bootDep7 BootstrapDependency[T7], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + dep3 := instanceOf(bs, bootDep3, index, embed) - dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + dep4 := instanceOf(bs, bootDep4, index, embed) - dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + dep5 := instanceOf(bs, bootDep5, index, embed) - dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + dep6 := instanceOf(bs, bootDep6, index, embed) - dep7 := &dependency[T7]{bd: bootDep7, bs: bs} + dep7 := instanceOf(bs, bootDep7, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7) - }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0)}, + ) } // Run9 bootstraps the services built from 9 dependencies. // fn receives the context, the StandaloneConfig, and the 9 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7], dep8 Dependency[T8]) []services.Service, @@ -245,34 +354,47 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any bootDep7 BootstrapDependency[T7], bootDep8 BootstrapDependency[T8], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + dep3 := instanceOf(bs, bootDep3, index, embed) - dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + dep4 := instanceOf(bs, bootDep4, index, embed) - dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + dep5 := instanceOf(bs, bootDep5, index, embed) - dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + dep6 := instanceOf(bs, bootDep6, index, embed) - dep7 := &dependency[T7]{bd: bootDep7, bs: bs} + dep7 := instanceOf(bs, bootDep7, index, embed) - dep8 := &dependency[T8]{bd: bootDep8, bs: bs} + dep8 := instanceOf(bs, bootDep8, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) - }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0)}, + ) } // Run10 bootstraps the services built from 10 dependencies. // fn receives the context, the StandaloneConfig, and the 10 dependencies, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7], dep8 Dependency[T8], dep9 Dependency[T9]) []services.Service, @@ -287,28 +409,36 @@ func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 an bootDep8 BootstrapDependency[T8], bootDep9 BootstrapDependency[T9], ) error { + return bs.run(func(index int, embed bool) instanceServices { - dep0 := &dependency[T0]{bd: bootDep0, bs: bs} + dep0 := instanceOf(bs, bootDep0, index, embed) - dep1 := &dependency[T1]{bd: bootDep1, bs: bs} + dep1 := instanceOf(bs, bootDep1, index, embed) - dep2 := &dependency[T2]{bd: bootDep2, bs: bs} + dep2 := instanceOf(bs, bootDep2, index, embed) - dep3 := &dependency[T3]{bd: bootDep3, bs: bs} + dep3 := instanceOf(bs, bootDep3, index, embed) - dep4 := &dependency[T4]{bd: bootDep4, bs: bs} + dep4 := instanceOf(bs, bootDep4, index, embed) - dep5 := &dependency[T5]{bd: bootDep5, bs: bs} + dep5 := instanceOf(bs, bootDep5, index, embed) - dep6 := &dependency[T6]{bd: bootDep6, bs: bs} + dep6 := instanceOf(bs, bootDep6, index, embed) - dep7 := &dependency[T7]{bd: bootDep7, bs: bs} + dep7 := instanceOf(bs, bootDep7, index, embed) - dep8 := &dependency[T8]{bd: bootDep8, bs: bs} + dep8 := instanceOf(bs, bootDep8, index, embed) - dep9 := &dependency[T9]{bd: bootDep9, bs: bs} + dep9 := instanceOf(bs, bootDep9, index, embed) - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8, dep9) - }, bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9) + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8, dep9) + } + }, + []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9}, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0), bootDep9.ForEmbedding(0)}, + ) } diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index 664d73b52..f9832a933 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -6,13 +6,15 @@ import ( "context" "fmt" "io" - "net/http" "os" "os/signal" + "reflect" + "strconv" "sync" "syscall" "github.com/hashicorp/go-plugin" + "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" "github.com/smartcontractkit/chainlink-common/pkg/beholder" @@ -26,39 +28,51 @@ import ( "github.com/grafana/pyroscope-go" ) -// StandaloneConfig holds the process-wide dependencies the Bootstrapper provides +// StandaloneConfig holds the per-instance dependencies the Bootstrapper provides // to the service factory passed to Run. type StandaloneConfig struct { // Logger is hclog-compatible like a LOOP plugin's: JSON on stderr with // @level/@message/@timestamp keys, so a go-plugin host (e.g. the core node) // can parse and re-level the entries when this process runs under one, - // while remaining plain JSON logs when run standalone. + // while remaining plain JSON logs when run standalone. Named after the + // instance when this process runs more than one. Logger logger.SugaredLogger - // BeholderClient is the process-wide telemetry client, nil unless - // CL_TELEMETRY_ENDPOINT is configured. Exposed so a factory can build its - // own otel instruments (meters, tracers) consistent with the rest of the - // process, the same way a LOOP plugin would reach beholder.GetClient(). + // BeholderClient is the process-wide telemetry client, nil unless telemetry is configured. + // Exposed so a factory can build its own otel instruments (meters, tracers) consistent with + // the rest of the process, the same way a LOOP plugin would reach beholder.GetClient(). BeholderClient *beholder.Client + + // MetricsRegisterer is where prometheus collectors belong. It is the default registerer for + // a single-instance run, and one wrapped with an "instance" label otherwise: registering the + // same collector twice fails, so without the label only the first instance's metrics would + // be recorded. + MetricsRegisterer prometheus.Registerer } type Bootstrapper struct { - root *cobra.Command - config *StandaloneConfig - commonConfig CommonConfig - profiler *pyroscope.Profiler // nil unless CL_PYROSCOPE_SERVER_ADDRESS is configured + root *cobra.Command + settings settings + config *StandaloneConfig + commonConfig CommonConfig + embedConfig embedConfig + observability observability + + profiler *pyroscope.Profiler // nil unless a pyroscope server is configured closersMu sync.Mutex - closers []io.Closer // resolved dependency values that implement io.Closer + closers []io.Closer // resolved dependency values and started instances that implement io.Closer } -// NewBootstrapper creates a new Bootstrapper using the cobra command as its root. -// Note that the RunE on the cobra command will be overwritten when Run is called, and the cobra command is provided only for the remaining fields. +// NewBootstrapper creates a new Bootstrapper using the cobra command as its root. The root is +// there to describe the binary - its name, its help, its own settings - and to hang the "run" and +// "embed" subcommands off when Run is called; it does not run anything itself. // -// It creates the hclog-compatible logger and, when CL_TELEMETRY_* env vars are -// configured, starts beholder telemetry with any otel views from opts; it exits -// on failure. The logger runs and supervises the services (health, lifecycle -// logging) and is available via Config for use before Run. +// It creates the hclog-compatible logger and registers the process-wide configuration - common +// settings plus telemetry, tracing, chip ingress, profiling and the metrics/health server. The +// values those configure are started when the command runs, since nothing is decoded until then. +// The logger runs and supervises the services (health, lifecycle logging) and is available via +// Logger for use before Run. func NewBootstrapper(root *cobra.Command, opts ...Option) *Bootstrapper { var s settings for _, opt := range opts { @@ -72,33 +86,34 @@ func NewBootstrapper(root *cobra.Command, opts ...Option) *Bootstrapper { } slggr := logger.Sugared(logger.Named(lggr, root.Name())) - beholderClient, err := startTelemetry(context.Background(), s.otelViews) - if err != nil { - slggr.Fatalf("Failed to start telemetry: %s", err) - } - - profiler, err := startProfiler(root.Name()) - if err != nil { - slggr.Fatalf("Failed to start profiler: %s", err) - } - bs := &Bootstrapper{ - root: root, - config: &StandaloneConfig{Logger: slggr, BeholderClient: beholderClient}, - profiler: profiler, + root: root, + settings: s, + config: &StandaloneConfig{Logger: slggr, MetricsRegisterer: prometheus.DefaultRegisterer}, + observability: defaultObservability(), + embedConfig: defaultEmbedConfig(), } - // Registered through pkg/config/flags like every other config struct, so --fake is - // documented alongside the settings it interacts with rather than being invisible to the - // generated docs. It stays at the top level: it is process-wide, not one dependency's. + + // Registered through pkg/config/flags like every other config struct, and at the top level + // since what it holds is process-wide rather than one dependency's. It has no settings at the + // moment, so this binds nothing; it stays so that the next one is bound, documented and + // reachable from every dependency without further wiring. if err := flags.RegisterCommandFlags(root, &bs.commonConfig, flags.DefaultTOMLOptions("CRE", "CL")); err != nil { slggr.Fatalf("Failed to register common flags: %s", err) } + for _, o := range bs.observability.namespaced() { + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = o.namespace + if err := flags.RegisterCommandFlags(root, o.target, opts); err != nil { + slggr.Fatalf("Failed to register %s flags: %s", o.namespace, err) + } + } return bs } -// close closes resolved dependencies (in reverse resolution order), stops -// profiling, flushes telemetry, and syncs logs: the counterpart to the setup -// in NewBootstrapper and to dependency resolution during run. +// close closes started instances and resolved dependencies (in reverse resolution +// order), stops profiling, flushes telemetry, and syncs logs: the counterpart to +// the setup in NewBootstrapper and to dependency resolution during run. func (b *Bootstrapper) close() { b.closersMu.Lock() closers := b.closers @@ -132,116 +147,306 @@ func (b *Bootstrapper) registerCloser(v any) { // Logger returns the logger instance. It is safe to call before running the binary func (b *Bootstrapper) Logger() logger.SugaredLogger { return b.config.Logger } -// run composes the services returned by factory into a single supervising -// service via services.Engine sub-services, so their health is aggregated the -// same way the rest of the stack does it (services.Config.NewSubServices + -// HealthReport). It starts them along with a health checker (registered -// against the aggregated root service) and, if CL_PROMETHEUS_PORT is set, a -// web server on that port serving /metrics, /debug/pprof, and /healthz + -// /readyz (backed by the health checker), then blocks until an interrupt, -// then closes everything in reverse. -func (b *Bootstrapper) run(factory func(ctx context.Context) []services.Service, commands ...BootstrapCommand) error { +// instanceServices builds the services of one instance, given that instance's StandaloneConfig. +type instanceServices func(ctx context.Context, cfg *StandaloneConfig) []services.Service + +// instantiator returns the factory building instance index's services. The generated Run helpers +// supply it, since only they know the dependencies' types; embed says whether to replace each +// dependency with its embedded form (BootstrapDependency.ForEmbedding) first. +type instantiator func(index int, embed bool) instanceServices + +// run wires up the subcommands that start the binary and executes the root command. +// +// Each subcommand runs instantiate once per instance, composing that instance's services into a +// single supervising service via services.Engine sub-services, so their health is aggregated the +// same way the rest of the stack does it (services.Config.NewSubServices + HealthReport). It +// starts them along with a health checker (registered against the aggregated root service) and, +// if a prometheus port is configured, that instance's web server serving /metrics, +// /debug/pprof and /healthz + /readyz, then blocks until an interrupt and closes everything in +// reverse. +func (b *Bootstrapper) run(instantiate instantiator, configured, embedded []BootstrapCommand) error { defer b.close() - if err := b.setupCommands(commands); err != nil { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + runCmd := &cobra.Command{ + Use: "run", + Short: "Run a single instance", + Long: `Runs one instance, resolving every dependency as it is configured. + +Its settings are its own: a dependency that an embedded instance replaces rather than configures +(rage networking, say) takes its settings here and nowhere else.`, + RunE: func(*cobra.Command, []string) error { + return b.runInstances(ctx, stop, 1, false, instantiate) + }, + } + + embedCmd := &cobra.Command{ + Use: "embed", + Short: "Run several instances in this process, with the networking between them skipped", + Long: `Runs --instances copies of everything "run" runs inside a single process. + +Dependencies serve each instance rather than being configured per instance: the transports between +instances are replaced by in-process ones, identities that would be read from a database are derived +from the instance index instead, and what state instances do keep is partitioned per instance. Each +instance reports its own health, on the configured prometheus port plus its index. + +So the settings here are not "run" settings plus a count: what an embedded instance derives or +replaces, it cannot be told, and only what it still needs is accepted.`, + RunE: func(*cobra.Command, []string) error { + return b.runInstances(ctx, stop, b.embedConfig.Instances, true, instantiate) + }, + } + + // Local to this subcommand: --instances means nothing to a single instance. + if err := flags.RegisterSubcommandFlags(embedCmd, "", &b.embedConfig, flags.DefaultTOMLOptions("CRE", "CL")); err != nil { return err } - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() + if err := b.setupCommands(runCmd, embedCmd, configured, embedded); err != nil { + return err + } - b.root.RunE = func(cmd *cobra.Command, args []string) error { - svcs := factory(ctx) - root, _ := services.Config{ - Name: "Bootstrap", - NewSubServices: func(logger.Logger) []services.Service { return svcs }, - }.NewServiceEngine(b.config.Logger) + // The root only describes the binary: it has no RunE, so a bare invocation prints the help + // listing the two ways to start it rather than silently picking one of them. + b.root.AddCommand(runCmd, embedCmd) - if err := root.Start(ctx); err != nil { - stop() - return err - } - defer func() { _ = root.Close() }() + return b.root.Execute() +} - checker, err := b.startHealthChecker(root) - if err != nil { - stop() - return err - } - defer func() { b.config.Logger.ErrorIfFn(checker.Close, "Failed to close health checker") }() +// runInstances starts count instances and blocks until shutdown. stop cancels ctx, so a failure +// part-way through starting them tears down the ones already running rather than leaving them +// wedged. +func (b *Bootstrapper) runInstances(ctx context.Context, stop context.CancelFunc, count int, embed bool, instantiate instantiator) error { + if count < 1 { + return fmt.Errorf("cannot run %d instances: at least one is required", count) + } - if port, ok, err := prometheusPort(); err != nil { + // Telemetry and profiling are started here rather than in NewBootstrapper because their + // configuration is not decoded until the command runs. Both are process-wide, so they are + // started once however many instances follow, and before any of them create instruments. + beholderClient, err := startTelemetry(ctx, b.observability, b.settings.otelViews) + if err != nil { + return fmt.Errorf("failed to start telemetry: %w", err) + } + b.config.BeholderClient = beholderClient + + b.profiler, err = startProfiler(b.root.Name(), b.observability.pyroscope) + if err != nil { + return fmt.Errorf("failed to start profiler: %w", err) + } + + for i := range count { + if err := b.startInstance(ctx, i, count, instantiate(i, embed)); err != nil { stop() - return err - } else if ok { - http.HandleFunc("/healthz", healthHandler(checker.IsHealthy)) - http.HandleFunc("/readyz", healthHandler(checker.IsReady)) - - web := loop.WebServerOpts{}.New(b.config.Logger, port) - if err := web.Start(ctx); err != nil { - stop() - return fmt.Errorf("failed to start prometheus web server: %w", err) - } - defer func() { b.config.Logger.ErrorIfFn(web.Close, "Failed to close prometheus web server") }() + return fmt.Errorf("failed to start instance %d: %w", i, err) } + } - if underPluginHost() { - // Launched by a go-plugin host (e.g. the core node): expose the empty - // LOOP so the host can supervise this process over gRPC (handshake + - // go-plugin's liveness health check). The started services run in - // this process, so that liveness reflects them. Blocks until the host - // shuts us down. - plugin.Serve(&plugin.ServeConfig{ - HandshakeConfig: loop.EmptyHandshakeConfig(), - Plugins: map[string]plugin.Plugin{loop.PluginEmptyName: &loop.EmptyLoop{}}, - GRPCServer: plugin.DefaultGRPCServer, - }) - return nil - } + if !embed && underPluginHost() { + // Launched by a go-plugin host (e.g. the core node): expose the empty + // LOOP so the host can supervise this process over gRPC (handshake + + // go-plugin's liveness health check). The started services run in + // this process, so that liveness reflects them. Blocks until the host + // shuts us down. + // + // Not for an embed run: a host supervises one plugin, and the instances of an embed run + // are this process's own business rather than something it can address. + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: loop.EmptyHandshakeConfig(), + Plugins: map[string]plugin.Plugin{loop.PluginEmptyName: &loop.EmptyLoop{}}, + GRPCServer: plugin.DefaultGRPCServer, + }) + return nil + } + + // Standalone: block until interrupted, then close. + <-ctx.Done() + return nil +} + +// startInstance starts one instance's services, health checker and web server, registering each +// for shutdown. Everything it registers is closed in reverse order by close, which puts an +// instance's services ahead of the dependencies they resolved during start. +func (b *Bootstrapper) startInstance(ctx context.Context, index, count int, factory instanceServices) error { + cfg := b.instanceConfig(index, count) + + root, _ := services.Config{ + Name: instanceName(index, count), + NewSubServices: func(logger.Logger) []services.Service { return factory(ctx, cfg) }, + }.NewServiceEngine(cfg.Logger) + + if err := root.Start(ctx); err != nil { + return err + } + b.registerCloser(root) + + checker, err := b.startHealthChecker(root) + if err != nil { + return err + } + b.registerCloser(checker) - // Standalone: block until interrupted, then close. - <-ctx.Done() + if b.observability.prometheus.disabled() { return nil } + // Instance i takes the configured port plus i, so several instances can serve their own + // health without being configured a port each. + web, err := startWebServer(ctx, cfg.Logger, b.observability.prometheus.portFor(index), checker) + if err != nil { + return err + } + b.registerCloser(web) + return nil +} - return b.root.Execute() +// instanceConfig is the StandaloneConfig handed to one instance's factory: the process-wide values, +// plus the logger and registerer that belong to that instance alone. +// +// It deliberately does not say which instance this is. A service is written once and knows nothing +// about embedding; everything that has to differ between instances is a dependency, replaced by its +// embedded form before the service is handed it (see BootstrapDependency.ForEmbedding). +func (b *Bootstrapper) instanceConfig(index, count int) *StandaloneConfig { + cfg := *b.config + if count == 1 { + return &cfg + } + + cfg.Logger = logger.Sugared(logger.Named(b.config.Logger, "instance."+strconv.Itoa(index))) + // Constant label rather than a registry per instance: the health metrics come from + // package-level promauto collectors on the default registry, which a private registry would + // not see, and registering the same collector a second time is an error. + cfg.MetricsRegisterer = prometheus.WrapRegistererWith( + prometheus.Labels{"instance": strconv.Itoa(index)}, prometheus.DefaultRegisterer) + return &cfg } -func (b *Bootstrapper) setupCommands(commands []BootstrapCommand) error { - allCommands := make([]BootstrapCommand, 0, len(commands)) - for _, cmd := range commands { - allCommands = append(allCommands, cmd) - allCommands = append(allCommands, cmd.Dependencies()...) +// instanceName names an instance's aggregated service. It carries the index when there is more +// than one instance, since the health metrics are labelled by service name and would otherwise +// collide between instances. +func instanceName(index, count int) string { + if count == 1 { + return "Bootstrap" } + return "Bootstrap." + strconv.Itoa(index) +} - for _, cmd := range allCommands { - opts := flags.DefaultTOMLOptions("CRE", "CL") - opts.Namespace = cmd.Namespace() - if err := flags.RegisterCommandFlags(b.root, cmd.Config(), opts); err != nil { +// setupCommands registers the configuration of every dependency on the command that resolves it: +// the dependencies as configured under `run`, the ones embedding replaces them with under `embed`, +// and anything both forms share on the root, where both inherit it. +// +// Registering per subcommand is what keeps each command's settings honest. A setting an embedded +// instance derives rather than reads - a listen address, a keystore password - is not offered +// under `embed` at all, instead of being offered and then quietly ignored; and a setting only an +// embedded instance has is offered, which it could not be if only the configured form were +// registered. +// +// A config instance is registered exactly once. Two commands binding one key would both point +// viper's binding for it at whichever registered last, so the value typed on the command actually +// running would be read from the other command's untouched flag - hence the shared ones going to +// the root rather than to each subcommand. +func (b *Bootstrapper) setupCommands(runCmd, embedCmd *cobra.Command, configured, embedded []BootstrapCommand) error { + configuredTargets, err := collectTargets(configured) + if err != nil { + return err + } + embeddedTargets, err := collectTargets(embedded) + if err != nil { + return err + } + configuredConfigs, embeddedConfigs := configSet(configuredTargets), configSet(embeddedTargets) + + for _, t := range configuredTargets { + // Shared by both forms - one dependency serving every instance, typically - so it belongs + // to the binary rather than to either way of starting it. + cmd := runCmd + if embeddedConfigs[t.config] { + cmd = b.root + } + if err := b.registerTarget(cmd, t); err != nil { + return err + } + } + + for _, t := range embeddedTargets { + if configuredConfigs[t.config] { + continue // registered on the root above + } + if err := b.registerTarget(embedCmd, t); err != nil { return err } } return nil } -// healthHandler adapts a services.HealthChecker.IsHealthy/IsReady-shaped func into -// an HTTP handler: 200 with each check's status when ok, 503 and the failing -// checks' errors otherwise. -func healthHandler(check func() (bool, map[string]error)) http.HandlerFunc { - return func(w http.ResponseWriter, _ *http.Request) { - ok, errs := check() - if !ok { - w.WriteHeader(http.StatusServiceUnavailable) - } - for name, err := range errs { - if err != nil { - fmt.Fprintf(w, "%s: %s\n", name, err) +// registerTarget binds t's settings to cmd: persistently when that is the root, so subcommands +// inherit them, and locally otherwise. +func (b *Bootstrapper) registerTarget(cmd *cobra.Command, t target) error { + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = t.namespace + if cmd == b.root { + return flags.RegisterCommandFlags(b.root, t.config, opts) + } + // opts.Namespace prefixes the flags as well as the keys, so a dependency's setting is + // --ocr.listen-addresses wherever it is registered: it is named after the dependency that owns + // it, not after the command that happens to accept it. + return flags.RegisterSubcommandFlags(cmd, t.namespace, t.config, opts) +} + +// target is one config instance to register, under the namespace of the dependency that owns it. +type target struct { + namespace string + config any +} + +// collectTargets walks commands and everything they depend on, in declaration order, returning each +// distinct config instance once - so a dependency two others share contributes its settings once +// rather than colliding with itself. +// +// A config must be a pointer, since it is what the configuration is decoded into, or nil when the +// dependency has nothing to configure. Anything else is rejected here rather than left to fail +// further along: it would also be unusable as a map key, which is how instances are told apart. +func collectTargets(commands []BootstrapCommand) ([]target, error) { + var targets []target + seen := map[any]bool{} + + var walk func(cmds []BootstrapCommand) error + walk = func(cmds []BootstrapCommand) error { + for _, cmd := range cmds { + if cmd == nil { + continue + } + + switch cfg := cmd.Config(); { + case cfg == nil: // nothing to configure + case reflect.ValueOf(cfg).Kind() != reflect.Pointer: + return fmt.Errorf("%T: Config must return a pointer to the settings, or nil, got %T", cmd, cfg) + case !seen[cfg]: + seen[cfg] = true + targets = append(targets, target{namespace: cmd.Namespace(), config: cfg}) + } + + if err := walk(cmd.Dependencies()); err != nil { + return err } } - if ok { - fmt.Fprintln(w, "ok") - } + return nil } + if err := walk(commands); err != nil { + return nil, err + } + + return targets, nil +} + +// configSet indexes targets by config instance, for asking whether the other form registers one too. +func configSet(targets []target) map[any]bool { + configs := make(map[any]bool, len(targets)) + for _, t := range targets { + configs[t.config] = true + } + return configs } // startHealthChecker builds a services.HealthChecker that mirrors reporter (usually @@ -277,18 +482,28 @@ func underPluginHost() bool { return os.Getenv(h.MagicCookieKey) == h.MagicCookieValue } -type CommonConfig struct { - // Kept out of the example config: the example shows a real run, and --fake selects a - // different set of dependencies than the ones it illustrates. - Fake bool `usage:"use fake dependencies instead of real ones" flagdocs:"noexample"` +// CommonConfig is the process-wide configuration every dependency is handed when it is resolved. +// It says nothing about which instance is resolving it: by then a dependency has already been +// replaced by the form that serves that instance, so there is no decision left for it to make. +type CommonConfig struct{} + +// embedConfig is the `embed` subcommand's own configuration. +type embedConfig struct { + Instances int `usage:"number of instances to run in this process"` } +func defaultEmbedConfig() embedConfig { return embedConfig{Instances: 1} } + type Dependency[T any] interface { Get(ctx context.Context) (T, error) } type BootstrapCommand interface { + // Config returns the settings to bind, as a pointer to the struct they are decoded into, or nil + // when there are none - which is the usual answer from an embedded dependency, having derived or + // replaced everything it would otherwise be told. Config() any + Dependencies() []BootstrapCommand // Namespace roots this dependency's configuration, so its settings group together and @@ -300,6 +515,25 @@ type BootstrapCommand interface { type BootstrapDependency[T any] interface { Get(ctx context.Context, c CommonConfig) (T, error) + + // ForEmbedding returns the dependency instance i of an embedded run resolves instead of this + // one. It is called once per instance, after the configuration has been decoded and before + // anything is resolved, and only for an embedded run - a single instance resolves the + // dependency exactly as configured. + // + // What it returns is already specific to instance i: everything embedding changes - a derived + // identity instead of a stored one, an in-process transport instead of a socket, a schema of + // its own instead of the shared one - is settled here, so Get has no instance to ask about and + // no mode to branch on. A dependency whose embedded form needs none of its settings is free to + // return something else entirely (see ocr.Host), which is also how a setting that only a real + // deployment needs stops being required. + // + // Return the receiver to be shared by every instance, which is what a dependency backed by + // one process-wide resource wants: sharing the dependency shares the single value it resolves + // to. Anything else returns a copy, deep-copying the configuration it adapts so that instances + // never write through to each other's settings. + ForEmbedding(i int) BootstrapDependency[T] + BootstrapCommand } @@ -318,6 +552,19 @@ func (d *dependency[T]) Get(ctx context.Context) (T, error) { return v, err } +// instanceOf resolves which dependency instance index uses, and wraps it for that instance's +// factory. Called by the generated Run helpers, once per dependency per instance. +// +// A single instance keeps the dependency as it was built rather than embedding it at index 0: +// `run` is what the configuration describes literally, and a dependency that partitions itself per +// instance should not quietly move a single run's state somewhere else. +func instanceOf[T any](bs *Bootstrapper, bd BootstrapDependency[T], index int, embed bool) *dependency[T] { + if embed { + bd = bd.ForEmbedding(index) + } + return &dependency[T]{bs: bs, bd: bd} +} + // OnceBootstrapper wraps a BootstrapDependency so that Get is evaluated at most // onceGet: the first call resolves the dependency and caches its (value, error), // and every subsequent call returns that same result without re-running Get @@ -343,3 +590,14 @@ func (o *onceBootstrapper[T]) Get(ctx context.Context, c CommonConfig) (T, error o.once.Do(func() { o.val, o.err = o.BootstrapDependency.Get(ctx, c) }) return o.val, o.err } + +// ForEmbedding gives each instance its own cache, since each resolves its own value - unless the +// wrapped dependency returns itself, meaning it is shared by every instance, in which case this +// wrapper (and the one value it caches) is shared too. +func (o *onceBootstrapper[T]) ForEmbedding(i int) BootstrapDependency[T] { + next := o.BootstrapDependency.ForEmbedding(i) + if next == o.BootstrapDependency { + return o + } + return OnceBootstrapper[T](next) +} diff --git a/libs/standalone/cmd_helpers.go b/libs/standalone/cmd_helpers.go index fe0547ed3..973eee13d 100644 --- a/libs/standalone/cmd_helpers.go +++ b/libs/standalone/cmd_helpers.go @@ -5,4 +5,23 @@ // flags.RegisterCommandFlags(cmd, &cfg, "CRE", "CL") from AddCommands, which binds each field // as a CLI flag, a viper key, and CRE_*/CL_* env vars, then decodes and validates it (see the // `validate:"..."` tags) before the command runs. +// +// A binary gets two ways to run, as subcommands of its own root command: +// +// - run: one instance, using the transports and stores it is configured with. What a deployed +// binary does, and what the root command does with no subcommand at all. +// - embed: --instances copies of the same thing inside one process, for running a DON on one +// machine without the setup a DON of real nodes needs. +// +// Embedding is arranged entirely by the dependencies. A service is written once and knows nothing +// about it: everything that cannot be shared between instances is resolved through a +// BootstrapDependency, and each instance is handed the dependency that serves it (ForEmbedding) +// before its services are built. So a listening port becomes the next port along, a database +// becomes a schema of its own, and a peer's identity and transport become a derived key and an +// in-process channel - none of which the service serving on that port, or reading that database, +// has to be told about. +// +// Nothing downstream of that has an instance to reason about: an embedded dependency is already +// specific to its instance when it is built, so neither the services (StandaloneConfig) nor the +// dependencies themselves (CommonConfig) are told which one they belong to. package standalone diff --git a/libs/standalone/config.go b/libs/standalone/config.go new file mode 100644 index 000000000..c13c9cfbc --- /dev/null +++ b/libs/standalone/config.go @@ -0,0 +1,157 @@ +package standalone + +import ( + "fmt" + "strings" + + "github.com/smartcontractkit/chainlink-common/pkg/config" +) + +// This file holds the process-wide observability configuration: telemetry, tracing, chip +// ingress, profiling, and the metrics/health web server. +// +// These used to be read straight from CL_* env vars, which meant they were invisible to the +// generated config docs, unsettable from a config file, and read before cobra had parsed +// anything. They are now ordinary config structs registered through pkg/config/flags like every +// dependency's, so each setting is a flag, a config-file key and an env var at once. +// +// The namespaces and field names below are chosen so the env vars flags generates are exactly +// the ones a go-plugin host (the core node) already sets - telemetry.endpoint binds +// CL_TELEMETRY_ENDPOINT, pyroscope.server-address binds CL_PYROSCOPE_SERVER_ADDRESS, and so on - +// so nothing that runs this binary today has to change. + +const ( + // Namespaces the observability configs are registered under. Also what makes their env var + // names match the ones the plugin host sets, so they are load-bearing rather than cosmetic. + telemetryNamespace = "telemetry" + tracingNamespace = "tracing" + chipIngressNamespace = "chip-ingress" + pyroscopeNamespace = "pyroscope" + prometheusNamespace = "prometheus" + + // Legacy env prefixes for the two map-valued telemetry settings. pkg/config/flags has no + // map support, so those are []string of key=value pairs here; a host sets one env var per + // entry instead, and readEnvPairs still picks those up. See TelemetryConfig.Attributes. + envTelemetryAttributePrefix = "CL_TELEMETRY_ATTRIBUTE_" + envTelemetryAuthHeaderPrefix = "CL_TELEMETRY_AUTH_HEADER_" +) + +// TelemetryConfig is the beholder client's configuration: where telemetry goes and how it +// authenticates. An empty Endpoint leaves telemetry off, and the global noop client in place, so +// instruments created by services record nothing. +type TelemetryConfig struct { + Endpoint string `usage:"OTLP gRPC endpoint telemetry is exported to; telemetry is disabled when unset"` + InsecureConnection bool `usage:"export telemetry over an insecure connection"` + CACertFile string `usage:"CA certificate file used to verify the telemetry endpoint"` + + // Attributes and AuthHeaders are key=value pairs ("env=staging") rather than maps, since + // flags cannot bind a map field. Entries from the legacy CL_TELEMETRY_ATTRIBUTE_ and + // CL_TELEMETRY_AUTH_HEADER_ env vars are merged in on top of these, so a plugin host + // setting one env var per entry keeps working. + Attributes []string `usage:"extra telemetry resource attributes, as key=value pairs" example:"['env=staging']"` + AuthHeaders []string `usage:"telemetry auth headers, as key=value pairs" flagdocs:"noexample"` + + AuthPubKeyHex string `usage:"public key the telemetry auth headers are derived from"` + AuthHeadersTTL config.Duration `usage:"how long generated telemetry auth headers are valid for"` + PrometheusBridgeEnabled bool `usage:"feed metrics registered on the prometheus registry into the telemetry pipeline"` +} + +// TracingConfig is the OTLP tracing configuration. Traces go to the telemetry endpoint, so +// Enabled does nothing unless TelemetryConfig.Endpoint is set too. +type TracingConfig struct { + Enabled bool `usage:"export traces to the telemetry endpoint"` + SamplingRatio float64 `usage:"fraction of traces sampled, from 0 to 1"` + TLSCertFile string `usage:"TLS certificate file used by the trace exporter"` +} + +// ChipIngressConfig points the beholder client's chip ingress emitter at an endpoint. Emitting is +// enabled by setting one. +type ChipIngressConfig struct { + Endpoint string `usage:"chip ingress gRPC endpoint; the emitter is disabled when unset"` + InsecureConnection bool `usage:"connect to chip ingress over an insecure connection"` +} + +// PyroscopeConfig configures continuous profiling. An empty ServerAddress leaves profiling off. +type PyroscopeConfig struct { + ServerAddress string `usage:"pyroscope server address; profiling is disabled when unset"` + AuthToken config.SecretString `usage:"pyroscope auth token" flagdocs:"noexample"` + Environment string `usage:"environment tag attached to profiles"` +} + +// PrometheusConfig is the web server serving /metrics, /debug/pprof and the health endpoints. +// +// Every instance serves its own, so under `embed` instance i listens on Port+i and reports that +// instance's health rather than an aggregate. The default keeps the previous behaviour of an +// unset CL_PROMETHEUS_PORT: no server at all. +type PrometheusConfig struct { + Port int `usage:"port serving /metrics, /debug/pprof, /healthz and /readyz; -1 disables the server, 0 asks the OS for an ephemeral port. Instance i of an embed run listens on this port plus i"` +} + +// disabled reports whether no web server should be started. +func (c PrometheusConfig) disabled() bool { return c.Port < 0 } + +// portFor is the port instance i serves on: the configured port plus i, so instances in one +// process do not collide over it. An ephemeral port is left as it is - every instance asking the +// OS for one already gets a distinct port, whereas adding to it would name specific low ports +// nobody chose. +func (c PrometheusConfig) portFor(index int) int { + if c.Port == 0 { + return 0 + } + return c.Port + index +} + +// observability is every process-wide observability config, registered together by +// NewBootstrapper and consumed once the command runs and they have been decoded. +type observability struct { + telemetry TelemetryConfig + tracing TracingConfig + chipIngress ChipIngressConfig + pyroscope PyroscopeConfig + prometheus PrometheusConfig +} + +// defaultObservability is what the flags are bound to and decoded into, so an unset setting keeps +// the value it is given here. +func defaultObservability() observability { + return observability{ + tracing: TracingConfig{SamplingRatio: 1}, + // Off unless configured, as an unset CL_PROMETHEUS_PORT was before this was a flag. + prometheus: PrometheusConfig{Port: -1}, + } +} + +// namespaced pairs each config with the namespace it is registered under, in the order the flags +// are registered. +func (o *observability) namespaced() []struct { + namespace string + target any +} { + return []struct { + namespace string + target any + }{ + {telemetryNamespace, &o.telemetry}, + {tracingNamespace, &o.tracing}, + {chipIngressNamespace, &o.chipIngress}, + {pyroscopeNamespace, &o.pyroscope}, + {prometheusNamespace, &o.prometheus}, + } +} + +// parsePairs turns key=value strings into a map, erroring on an entry without a "=". Values may +// themselves contain "=", so only the first one separates. +func parsePairs(setting string, pairs []string) (map[string]string, error) { + if len(pairs) == 0 { + return nil, nil + } + m := make(map[string]string, len(pairs)) + for _, pair := range pairs { + key, value, found := strings.Cut(pair, "=") + if !found || key == "" { + return nil, fmt.Errorf("invalid %s entry %q: expected key=value", setting, pair) + } + m[key] = value + } + return m, nil +} diff --git a/libs/standalone/config_test.go b/libs/standalone/config_test.go new file mode 100644 index 000000000..423738d12 --- /dev/null +++ b/libs/standalone/config_test.go @@ -0,0 +1,70 @@ +package standalone + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPrometheusConfigPortFor(t *testing.T) { + cfg := PrometheusConfig{Port: 9090} + assert.Equal(t, 9090, cfg.portFor(0)) + assert.Equal(t, 9092, cfg.portFor(2)) + assert.False(t, cfg.disabled()) + + t.Run("an ephemeral port stays ephemeral for every instance", func(t *testing.T) { + // Offsetting 0 would turn "any free port" into port 3, which nobody asked for and which an + // unprivileged process may not even be able to bind. + ephemeral := PrometheusConfig{Port: 0} + assert.Equal(t, 0, ephemeral.portFor(3)) + assert.False(t, ephemeral.disabled()) + }) + + t.Run("the default leaves the server off, as an unset CL_PROMETHEUS_PORT did", func(t *testing.T) { + assert.True(t, defaultObservability().prometheus.disabled()) + }) +} + +func TestParsePairs(t *testing.T) { + pairs, err := parsePairs("telemetry.attributes", []string{"env=staging", "region=eu-west-1"}) + require.NoError(t, err) + assert.Equal(t, map[string]string{"env": "staging", "region": "eu-west-1"}, pairs) + + t.Run("a value may contain =", func(t *testing.T) { + pairs, err := parsePairs("telemetry.auth-headers", []string{"authorization=Basic dXNlcjpwYXNz=="}) + require.NoError(t, err) + assert.Equal(t, map[string]string{"authorization": "Basic dXNlcjpwYXNz=="}, pairs) + }) + + t.Run("nothing configured is not an empty map", func(t *testing.T) { + pairs, err := parsePairs("telemetry.attributes", nil) + require.NoError(t, err) + assert.Nil(t, pairs) + }) + + for _, invalid := range []string{"noequals", "=novalue"} { + t.Run("rejects "+invalid, func(t *testing.T) { + _, err := parsePairs("telemetry.attributes", []string{invalid}) + require.ErrorContains(t, err, "expected key=value") + }) + } +} + +func TestEnvPairsMergesLegacyEnvVars(t *testing.T) { + // A plugin host encodes a map as one env var per entry, which is how these settings arrived + // before they were flags; both sources have to reach the client. + t.Setenv(envTelemetryAttributePrefix+"from_env", "yes") + t.Setenv(envTelemetryAttributePrefix+"overridden", "by env") + + pairs, err := envPairs(envTelemetryAttributePrefix, "telemetry.attributes", + []string{"from_setting=yes", "overridden=by setting"}) + require.NoError(t, err) + + assert.Equal(t, map[string]string{ + "from_env": "yes", + "from_setting": "yes", + // The setting wins, being the more specific source. + "overridden": "by setting", + }, pairs) +} diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index 9cd3fce60..3a74fa70b 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -5,83 +5,149 @@ import ( "database/sql" "fmt" "io/fs" + "net/url" + "strings" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/capabilities/libs/standalone" ) -// Config is the database configuration: the connection settings that belong to the database -// itself (sqlutil.Config, which also knows how to open it), plus the one setting that only -// exists because this framework has a --fake mode. +// Dependency returns a standalone.BootstrapDependency that resolves an opened, migrated database. +// The connection settings belong to the database itself (sqlutil.Config, which also knows how to +// open it), so they are bound directly rather than wrapped: this dependency adds no setting of its +// own. // -// Both settings are related to --fake, which this struct does not own: -// -// - real-db only means something in fake mode, so it is rejected without --fake. -// - url is needed whenever a real database is actually used: always in normal mode, and in -// fake mode only when --real-db asks for one. -// -// Neither rule is expressible as a `validate` tag (both reference --fake), so neither shows up -// in the generated docs on its own - the usage text spells them out so the docs still explain -// when each setting applies. -type Config struct { - // Embedded as a pointer so this struct adds to the shared settings rather than copying - // them; inline so its fields sit alongside real-db under database.* instead of nesting. - *sqlutil.Config `toml:",inline"` - - // Kept out of the example config: the example shows a normal run against a real database, - // where this setting does not apply. It is still documented. - RealDB bool `usage:"use a real database even though --fake is set; requires --fake, and a url to point at" flagdocs:"noexample"` -} - +// Each embedded instance gets its own schema (see ForEmbedding), so the instances of an embedded +// DON keep their state apart while sharing one server, one URL and one set of credentials. func Dependency(migrationsFS fs.FS, migrationTable string) standalone.BootstrapDependency[*sql.DB] { return standalone.OnceBootstrapper[*sql.DB](&dependency{ migrationsFS: migrationsFS, migrationTable: migrationTable, - // The embedded pointer is non-nil so the shared settings are bound and defaulted from - // here, the same way every other dependency starts from its config's defaults. - cfg: Config{Config: &sqlutil.Config{}}, + // The instance the flags are bound to and decoded into, so an unset setting keeps the + // value it is given here. Fresh per call rather than shared, as every other dependency's + // config is. + cfg: &sqlutil.Config{}, }) } type dependency struct { - db *sql.DB - cfg Config + cfg *sqlutil.Config migrationsFS fs.FS migrationTable string } func (d *dependency) Config() any { - return &d.cfg + return d.cfg } func (d *dependency) Dependencies() []standalone.BootstrapCommand { return []standalone.BootstrapCommand{} } -func (d *dependency) Get(ctx context.Context, commonConfig standalone.CommonConfig) (*sql.DB, error) { - if commonConfig.Fake && !d.cfg.RealDB { - if d.cfg.URL != "" { - return nil, fmt.Errorf("database url set when using fake database") - } - // TODO set db to an in-memory one - // Also add a subcommand to override with real DB even if fake is used - } +// ForEmbedding returns a dependency that keeps its tables in a schema of its own, node_. +// +// A database is the one dependency embedded instances must not share as it is: they are separate +// nodes, and separate nodes with one set of tables would read each other's rows. Partitioning by +// schema rather than by database (or by URL) keeps that to one connection string and one set of +// credentials, and puts every instance's state where an operator can see it side by side. +// +// Instance 0 is partitioned like the rest, into node_0, rather than being left on the configured +// schema: a run of N instances is N nodes, and having one of them live somewhere else would make its +// state the odd one out in exactly the situation the schemas exist to keep tidy. A plain `run` never +// calls this, so it is untouched by any of it. +func (d *dependency) ForEmbedding(i int) standalone.BootstrapDependency[*sql.DB] { + return &embedded{dependency: d, schema: fmt.Sprintf("node_%d", i)} +} - var err error - d.db, err = sqlutil.OpenDB(*d.cfg.Config) +// Get opens the database as configured. +func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (*sql.DB, error) { + return d.open(ctx, *d.cfg, nil) +} + +// open opens the database at cfg and applies the migrations, running prepare (when given) on the +// opened database first - which is where an embedded instance creates the schema its migrations +// land in. +func (d *dependency) open(ctx context.Context, cfg sqlutil.Config, prepare func(context.Context, *sql.DB) error) (*sql.DB, error) { + db, err := sqlutil.OpenDB(cfg) if err != nil { return nil, err } - if err = migrate(ctx, d.db, d.migrationsFS, d.migrationTable); err != nil { - return nil, err + if prepare != nil { + if err := prepare(ctx, db); err != nil { + return nil, err + } } - return d.db, nil + if err := migrate(ctx, db, d.migrationsFS, d.migrationTable); err != nil { + return nil, err + } + return db, nil } // Namespace groups the database settings under database.* (--database.url, CRE_DATABASE_URL). func (d *dependency) Namespace() string { return "database" } var _ standalone.BootstrapDependency[*sql.DB] = (*dependency)(nil) + +// embedded is one embedded instance's database: the configured server, with this instance's tables +// in a schema of its own. Which schema is settled when it is built, so resolving it asks no +// questions about who is resolving it. +type embedded struct { + *dependency + schema string +} + +var _ standalone.BootstrapDependency[*sql.DB] = (*embedded)(nil) + +func (d *embedded) Get(ctx context.Context, _ standalone.CommonConfig) (*sql.DB, error) { + cfg := *d.cfg + var err error + if cfg.URL, err = withSearchPath(cfg.URL, d.schema); err != nil { + return nil, err + } + + return d.open(ctx, cfg, func(ctx context.Context, db *sql.DB) error { + // The search path names the schema but does not bring it into being, and an instance is not + // something an operator sets up by hand. + if _, err := db.ExecContext(ctx, `CREATE SCHEMA IF NOT EXISTS `+quoteIdentifier(d.schema)); err != nil { + return fmt.Errorf("failed to create schema %s: %w", d.schema, err) + } + return nil + }) +} + +// withSearchPath returns rawURL with schema at the front of its search path, so unqualified names +// resolve to (and new tables are created in) that schema. public is kept behind it, since anything +// shared by the whole database - extensions, most obviously - lives there and every instance still +// needs to reach it. An operator's own search_path is preserved the same way, with the instance's +// schema taking precedence over it. +// +// pgx passes query parameters it does not recognise to the server as runtime parameters, which is +// how a schema is selected for a pool without having to set it on every connection by hand. +func withSearchPath(rawURL, schema string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return "", fmt.Errorf("failed to parse database url: %w", err) + } + + query := u.Query() + searchPath := []string{schema} + if existing := query.Get("search_path"); existing != "" { + searchPath = append(searchPath, existing) + } else { + searchPath = append(searchPath, "public") + } + query.Set("search_path", strings.Join(searchPath, ",")) + u.RawQuery = query.Encode() + + return u.String(), nil +} + +// quoteIdentifier quotes a postgres identifier. The schema names here are built from an instance +// index and cannot contain anything that needs escaping, but an identifier interpolated into DDL is +// quoted regardless: the next name to be interpolated may not be as safe. +func quoteIdentifier(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +} diff --git a/libs/standalone/db/db_dependency_test.go b/libs/standalone/db/db_dependency_test.go new file mode 100644 index 000000000..eaa72c818 --- /dev/null +++ b/libs/standalone/db/db_dependency_test.go @@ -0,0 +1,63 @@ +package db + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" +) + +func TestWithSearchPath(t *testing.T) { + const dsn = "postgresql://user:password@localhost:5432/chainlink?sslmode=disable" + + t.Run("the instance's schema is searched before public", func(t *testing.T) { + withSchema, err := withSearchPath(dsn, "node_1") + require.NoError(t, err) + + // public stays reachable behind it: extensions and anything else shared by the whole + // database live there, and every instance still needs them. + assert.Equal(t, "node_1,public", queryParam(t, withSchema, "search_path")) + // Everything else about the connection is left as configured. + assert.Equal(t, "disable", queryParam(t, withSchema, "sslmode")) + assert.Equal(t, "/chainlink", mustParse(t, withSchema).Path) + assert.Equal(t, "user:password", mustParse(t, withSchema).User.String()) + }) + + t.Run("a configured search path is kept, behind the instance's schema", func(t *testing.T) { + withSchema, err := withSearchPath(dsn+"&search_path=shared", "node_2") + require.NoError(t, err) + + assert.Equal(t, "node_2,shared", queryParam(t, withSchema, "search_path")) + }) + + t.Run("an unparseable url is an error", func(t *testing.T) { + _, err := withSearchPath("postgresql://user@%zz/db", "node_0") + require.ErrorContains(t, err, "failed to parse database url") + }) +} + +func TestForEmbedding(t *testing.T) { + const dsn = "postgresql://localhost:5432/chainlink" + template := &dependency{cfg: &sqlutil.Config{URL: dsn}, migrationTable: "migrations"} + + // Instance 0 is partitioned like every other instance, so a run of N instances has N schemas and + // none of them is the odd one out. A single instance never calls this, and keeps the database as + // configured. + assert.Equal(t, "node_0", template.ForEmbedding(0).(*embedded).schema) + assert.Equal(t, "node_1", template.ForEmbedding(1).(*embedded).schema) +} + +func mustParse(t *testing.T, rawURL string) *url.URL { + t.Helper() + u, err := url.Parse(rawURL) + require.NoError(t, err) + return u +} + +func queryParam(t *testing.T, rawURL, key string) string { + t.Helper() + return mustParse(t, rawURL).Query().Get(key) +} diff --git a/libs/standalone/evm/dependency.go b/libs/standalone/evm/dependency.go index 8bbf662e8..e2930c761 100644 --- a/libs/standalone/evm/dependency.go +++ b/libs/standalone/evm/dependency.go @@ -57,6 +57,11 @@ func (d *dependency) Dependencies() []standalone.BootstrapCommand { return []standalone.BootstrapCommand{} } +// ForEmbedding returns the receiver, so every embedded instance shares one client: they are +// reading the same chain over the same RPCs, and a client per instance would multiply the request +// rate without telling any of them anything new. +func (d *dependency) ForEmbedding(int) standalone.BootstrapDependency[evmclient.Client] { return d } + func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmclient.Client, error) { cl, err := evmclient.NewClientFromConfig(ctx, d.lggr, d.cfg) if err != nil { diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl index 1c7050071..224b219b4 100644 --- a/libs/standalone/gen/bootstrap.go.tmpl +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -12,6 +12,11 @@ import ( // fn receives the context, the StandaloneConfig, and the {{.}} {{if eq . 1}}dependency{{else}}dependencies{{end}}, and returns the services to run. // The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. // The returned services are started together and their health is aggregated by the bootstrapper. +// +// fn is called once per instance: a single time for `run`, and once for each of the instances +// `embed` starts. For an embedded instance every dependency is first replaced by the form serving +// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is +// already theirs. func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, {{range RangeNum .}}dep{{.}} Dependency[T{{.}}], {{end}}) []services.Service, @@ -19,11 +24,19 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( bootDep{{.}} BootstrapDependency[T{{.}}], {{- end}} ) error { - {{range RangeNum .}} - dep{{.}} := &dependency[T{{.}}]{bd: bootDep{{.}}, bs: bs} - {{end}} - return bs.run(func(ctx context.Context) []services.Service { - return fn(ctx, bs.config, {{range RangeNum .}}dep{{.}}, {{end}}) - }, {{range RangeNum .}}bootDep{{.}},{{end}}) + return bs.run(func(index int, embed bool) instanceServices { + {{range RangeNum .}} + dep{{.}} := instanceOf(bs, bootDep{{.}}, index, embed) + {{end}} + return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { + return fn(ctx, cfg, {{range RangeNum .}}dep{{.}}, {{end}}) + } + }, + []BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}, {{end}} }, + // The embedded form of each dependency, so its settings are registered on the command that + // resolves it. Instance 0's, since a form is asked for its settings and not for its index; + // each instance builds its own when it starts. + []BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}.ForEmbedding(0), {{end}} }, + ) } {{- end }} diff --git a/libs/standalone/listener/dependency.go b/libs/standalone/listener/dependency.go new file mode 100644 index 000000000..05f80f92e --- /dev/null +++ b/libs/standalone/listener/dependency.go @@ -0,0 +1,108 @@ +// Package listener provides a standalone.BootstrapDependency that resolves the net.Listener a +// server serves on. +// +// A listening port is the one thing several instances of a binary in one process cannot share, and +// this is where that is dealt with: instance i listens one port along, so a process running an +// embedded DON needs no per-instance configuration and the service that serves on the listener +// never learns which instance it belongs to. That is the point of resolving it as a dependency +// rather than having a service open a port from an address it was configured with - a service +// aware of its own instance index would have to be written twice, once for the single-instance +// case and once for the embedded one. +package listener + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +// Config is where the server listens. +type Config struct { + ListenAddress string `usage:"address (host:port) this server listens on; instance i of an embed run listens on the port plus i"` +} + +// Dependency returns a standalone.BootstrapDependency that resolves a listening TCP socket. +// +// namespace groups the setting, so a binary serving on more than one address gives each its own +// ("proxy" yields --proxy.listen-address). defaultAddress is what it listens on when nothing is +// configured; pass "" to require the address to be set. +func Dependency(namespace, defaultAddress string) standalone.BootstrapDependency[net.Listener] { + // Wrap in OnceBootstrapper so the port is bound at most once even if several services resolve + // this dependency - two listeners on one address is an error, and the second service would be + // the one to fail. + return standalone.OnceBootstrapper[net.Listener](&dependency{ + namespace: namespace, + cfg: &Config{ListenAddress: defaultAddress}, + }) +} + +type dependency struct { + namespace string + // cfg is shared with the embedded forms rather than copied into them: it is the instance the + // flags are bound to, so a copy taken before decoding would never see the configured address. + cfg *Config + // portOffset is added to the configured port, set by ForEmbedding. Zero for a single instance, + // which listens exactly where it is configured to. + portOffset int +} + +var _ standalone.BootstrapDependency[net.Listener] = (*dependency)(nil) + +func (d *dependency) Namespace() string { return d.namespace } + +func (d *dependency) Config() any { return d.cfg } + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{} +} + +// ForEmbedding returns a form that listens i ports along from the configured address, since a port +// is the one thing two instances in one process cannot share. It reads the same settings as the +// receiver - an address, wherever it came from - so those stay one shared config instance, +// registered once and inherited by both commands. +func (d *dependency) ForEmbedding(i int) standalone.BootstrapDependency[net.Listener] { + clone := *d + clone.portOffset = i + return &clone +} + +func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (net.Listener, error) { + if d.cfg.ListenAddress == "" { + return nil, fmt.Errorf("--%s.listen-address is required", d.namespace) + } + + address, err := standalone.OffsetPort(d.cfg.ListenAddress, d.portOffset) + if err != nil { + return nil, err + } + + // Bound through a ListenConfig so the context governs it, as it does every other resolution. + var lc net.ListenConfig + lis, err := lc.Listen(ctx, "tcp", address) + if err != nil { + return nil, fmt.Errorf("failed to listen on %s: %w", address, err) + } + return &listener{Listener: lis}, nil +} + +// listener makes Close idempotent. The bootstrapper closes every resolved dependency on shutdown, +// and a server given a listener closes it as part of stopping, so whichever gets there second +// would otherwise report an already-closed socket as a shutdown failure. +type listener struct { + net.Listener + once sync.Once + err error +} + +func (l *listener) Close() error { + l.once.Do(func() { + if err := l.Listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + l.err = err + } + }) + return l.err +} diff --git a/libs/standalone/listener/dependency_test.go b/libs/standalone/listener/dependency_test.go new file mode 100644 index 000000000..1b6de791c --- /dev/null +++ b/libs/standalone/listener/dependency_test.go @@ -0,0 +1,71 @@ +package listener + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +func TestDependency(t *testing.T) { + t.Run("a single-instance run listens on the configured address", func(t *testing.T) { + // Port 0 so the test binds something it is guaranteed to be able to. + dep := Dependency("proxy", "127.0.0.1:0") + + lis, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, lis.Close()) }) + + assert.Equal(t, "127.0.0.1", lis.Addr().(*net.TCPAddr).IP.String()) + }) + + t.Run("each instance listens one port along", func(t *testing.T) { + // Bind an ephemeral port first, then build addresses relative to it, so the ports this + // test needs are ones the OS has just told us are free. + base, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0") + require.NoError(t, err) + require.NoError(t, base.Close()) + basePort := base.Addr().(*net.TCPAddr).Port + + template := Dependency("proxy", net.JoinHostPort("127.0.0.1", strconv.Itoa(basePort))) + + for i := range 2 { + lis, err := template.ForEmbedding(i).Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, lis.Close()) }) + + assert.Equal(t, basePort+i, lis.Addr().(*net.TCPAddr).Port) + } + }) + + t.Run("an unset address is an error naming the flag", func(t *testing.T) { + _, err := Dependency("proxy", "").Get(t.Context(), standalone.CommonConfig{}) + require.ErrorContains(t, err, "--proxy.listen-address is required") + }) + + t.Run("closing twice is not a failure", func(t *testing.T) { + lis, err := Dependency("proxy", "127.0.0.1:0").Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + + // The bootstrapper closes every resolved dependency, and a server handed a listener closes + // it while stopping; whichever is second must not report a shutdown failure. + require.NoError(t, lis.Close()) + require.NoError(t, lis.Close()) + }) + + t.Run("the same dependency binds one port however many services resolve it", func(t *testing.T) { + dep := Dependency("proxy", "127.0.0.1:0") + + first, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, first.Close()) }) + + second, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + assert.Same(t, first, second) + }) +} diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index 54f4546fd..19864e421 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -1,48 +1,44 @@ -// Package ocr provides a standalone.BootstrapDependency that supplies the -// libocr rage networking factories (OCR endpoint, OCR3.1 endpoint, and -// DON-to-DON peer group) a standalone binary needs. +// Package ocr provides the standalone.BootstrapDependency a binary uses to obtain the libocr rage +// networking factories (OCR endpoint, OCR3.1 endpoint, and DON-to-DON peer group). // -// It has two mutually exclusive modes, mirroring core's SingletonPeerWrapper: +// There are two, mirroring the two halves of core's SingletonPeerWrapper, and a binary picks one +// by calling its constructor: // -// - create: build a local libocr peer (networking.NewPeer) and expose its -// factories. Requires --ocr.listen-addresses and uses the node's P2P identity -// and OCR discoverer table from the database. -// - proxy: delegate rage networking to an out-of-process proxy at -// --ocr.proxy-address, exposing proxy-client-backed factories instead of a -// local peer. +// - Host: create a local libocr peer (networking.NewPeer) and expose its factories. Unlocks the +// node's P2P identity from the keystore in the database it shares with the node, and uses +// --ocr.listen-addresses and the OCR discoverer table in that same database. +// - Proxy: delegate rage networking to an out-of-process host at --ocr.proxy-address, exposing +// proxy-client-backed factories instead of a local peer. Needs no database and no keystore +// password: it is told the peer ID the proxy hosts for it. // -// Exactly one of --ocr.listen-addresses / --ocr.proxy-address may (and must) be -// provided; the peer's own settings come from chainlink-common's ocrcommon.Config, -// which this package's Config embeds and adds proxy mode to. +// Which of the two a binary is, is a property of the binary rather than a setting: a p2p proxy +// server hosts a peer, and a process fronted by one delegates to it. Expressing that by +// construction rather than by a mode flag means neither ever has to reject the other's settings, +// the settings a binary does have are the ones that apply to it, and the keystore password stays +// with the one process that has any use for it. +// +// Embedding replaces both with the same thing: the in-process transport (see inproc.go) under an +// identity derived from the instance index (see keyring.go and embed.go). Instances sharing a +// process have no network between them, and no node keystore to borrow an identity from, so neither +// form's settings apply and neither is required. package ocr import ( - "context" - "database/sql" - "fmt" "io" "time" - "github.com/jmoiron/sqlx" - "github.com/prometheus/client_golang/prometheus" - - "github.com/smartcontractkit/libocr/networking" - "github.com/smartcontractkit/libocr/networking/rageping" ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/config" - commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" - - "github.com/smartcontractkit/capabilities/libs/standalone" ) -// Factories bundles the libocr rage networking factories the caller serves or -// drives. It is produced by either the create or proxy mode; the caller does -// not need to know which. Close tears down the underlying peer (create mode) or -// proxy client connections (proxy mode). +// Factories bundles the libocr rage networking factories the caller serves or drives. Any of the +// forms above produces it; the caller does not need to know which. Close tears down the underlying +// peer (a hosted one) or the proxy client connections (a delegating one), and nothing at all for an +// embedded instance, which holds neither. type Factories struct { // OCR2Endpoint creates OCR2 BinaryNetworkEndpoints. OCR2Endpoint ocr2types.BinaryNetworkEndpointFactory @@ -51,9 +47,9 @@ type Factories struct { // PeerGroup creates DON-to-DON peer groups. PeerGroup creproxy.PeerGroupFactory - // PeerID is the node's rage P2P identity, loaded from the keystore. Both - // modes resolve it, and consumers other than libocr need it: the on-chain - // CapabilitiesRegistry keys node records by peer ID, so anything reading + // PeerID is the node's rage P2P identity: unlocked from the keystore, configured directly, or + // derived from the instance index. Every form resolves it, and consumers other than libocr need + // it: the on-chain CapabilitiesRegistry keys node records by peer ID, so anything reading // registry metadata must know which node it is. PeerID ragetypes.PeerID @@ -68,161 +64,16 @@ func (f *Factories) Close() error { return f.closer.Close() } -// Dependency returns a standalone.BootstrapDependency that resolves the libocr -// Factories. It wraps the database dependency, which it uses to load the node's -// P2P identity (both modes) and the OCR discoverer database (create mode). -// discovererTable is the migration-created table backing p2p announcements. -func Dependency(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*Factories] { - // Wrap in OnceBootstrapper so Get (which creates the peer or proxy clients) - // runs at most once even if several services resolve this dependency. - return standalone.OnceBootstrapper[*Factories](&dependency{lggr: lggr, db: db, discovererTable: discovererTable, cfg: defaultConfig()}) -} - -// Config is the libocr networking configuration: the settings of the local peer itself -// (ocrcommon.Config, shared with anything else that runs one), plus proxy mode, which is this -// framework's own alternative to running that peer. -// -// The two modes are expressed as validator tags rather than cobra's -// MarkFlagsMutuallyExclusive/MarkFlagsOneRequired: those only inspect whether a flag was -// literally typed on the command line, so they would reject a mode that was selected via a -// config file or env var. required_without/excluded_with are checked against the decoded -// values instead, so exactly-one-of holds no matter which source supplied it. Both rules sit on -// ProxyAddress, the field this struct owns: a rule on the embedded ListenAddresses could not -// name a field outside its own struct. -type Config struct { - // Embedded as a pointer so this struct adds proxy mode to the shared peer settings rather - // than copying them; inline so its fields sit alongside proxy-address under ocr.* instead - // of nesting. - *ocrcommon.Config `toml:",inline"` - - // proxy-mode config - ProxyAddress string `usage:"delegate rage networking to a proxy at this gRPC address instead of creating a local peer" validate:"required_without=ListenAddresses,excluded_with=ListenAddresses"` -} - -// defaultConfig is the instance the flags are bound to and decoded into, so an unset setting -// keeps the value it is given here. The embedded pointer is fresh per call rather than a shared -// package-level value, so two dependencies can never decode into the same peer settings. Proxy -// mode has no default: an unset proxy address is what selects a local peer. -func defaultConfig() Config { - return Config{Config: &ocrcommon.Config{ +// defaultPeerConfig is the peer settings the flags are bound to and decoded into, so an unset +// setting keeps the value it is given here. Freshly allocated per call rather than shared, so two +// dependencies (or two instances of one) can never decode into the same peer settings. +func defaultPeerConfig() *ocrcommon.Config { + return &ocrcommon.Config{ DeltaReconcile: *config.MustNewDuration(time.Minute), DeltaDial: *config.MustNewDuration(5 * time.Second), IncomingBufferSize: 100, OutgoingBufferSize: 100, - }} -} - -type dependency struct { - lggr commonlogger.Logger - db standalone.BootstrapDependency[*sql.DB] - discovererTable string - - cfg Config -} - -var _ standalone.BootstrapDependency[*Factories] = (*dependency)(nil) - -// Namespace groups the libocr networking settings under ocr.* (--ocr.listen-addresses, -// CRE_OCR_LISTEN_ADDRESSES). -func (d *dependency) Namespace() string { return "ocr" } - -func (d *dependency) Config() any { - return &d.cfg -} - -func (d *dependency) Dependencies() []standalone.BootstrapCommand { - return []standalone.BootstrapCommand{d.db} -} - -func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Factories, error) { - sqlDB, err := d.db.Get(ctx, cc) - if err != nil { - return nil, fmt.Errorf("failed to get database: %w", err) - } - ds := sqlx.NewDb(sqlDB, "pgx") - - // Both modes use the node's own P2P identity so this process is the same - // peer as the node it fronts. - keyring, err := loadPeerKeyring(ctx, ds, string(d.cfg.KeystorePassword)) - if err != nil { - return nil, err - } - peerID := ragetypes.PeerIDFromKeyring(keyring) - - if d.cfg.ProxyAddress != "" { - return d.proxyFactories(peerID) - } - return d.localFactories(ds, keyring, peerID) -} - -// localFactories builds a real libocr peer and exposes its factories. -func (d *dependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*Factories, error) { - discovererDB := ocrcommon.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) - - d.lggr.Infow("Creating local p2p peer", - "peerID", peerID.String(), - "listenAddresses", d.cfg.ListenAddresses, - "announceAddresses", d.cfg.AnnounceAddresses, - ) - - peer, err := networking.NewPeer(networking.PeerConfig{ - PeerKeyring: keyring, - Logger: commonlogger.NewOCRWrapper(d.lggr, false, func(string) {}), - V2ListenAddresses: d.cfg.ListenAddresses, - V2AnnounceAddresses: d.cfg.AnnounceAddresses, - V2DeltaReconcile: d.cfg.DeltaReconcile.Duration(), - V2DeltaDial: d.cfg.DeltaDial.Duration(), - V2DiscovererDatabase: discovererDB, - V2EndpointConfig: networking.EndpointConfigV2{ - IncomingMessageBufferSize: d.cfg.IncomingBufferSize, - OutgoingMessageBufferSize: d.cfg.OutgoingBufferSize, - }, - MetricsRegisterer: prometheus.DefaultRegisterer, - LatencyMetricsServiceConfigs: rageping.DefaultConfigs(), - }) - if err != nil { - return nil, fmt.Errorf("failed to create rage peer: %w", err) - } - - return &Factories{ - OCR2Endpoint: peer.OCR2BinaryNetworkEndpointFactory(), - OCR3_1Endpoint: peer.OCR3_1BinaryNetworkEndpointFactory(), - PeerGroup: newNetworkingPeerGroupFactory(peer.PeerGroupFactory()), - PeerID: peerID, - closer: peer, - }, nil -} - -// proxyFactories delegates rage networking to an out-of-process proxy: no local -// peer is created; the factories are backed by proxy clients connected to -// d.cfg.ProxyAddress. The node's raw peer ID is passed to the endpoint factories, -// as libocr compares it against the peer IDs in the OCR config. -func (d *dependency) proxyFactories(peerID ragetypes.PeerID) (*Factories, error) { - endpointFactory, err := creproxy.NewProxyEndpointFactory(peerID.String(), d.cfg.ProxyAddress) - if err != nil { - return nil, fmt.Errorf("failed to create proxy OCR endpoint factory: %w", err) } - endpoint2Factory, err := creproxy.NewProxyEndpoint2Factory(peerID.String(), d.cfg.ProxyAddress) - if err != nil { - _ = endpointFactory.Close() - return nil, fmt.Errorf("failed to create proxy OCR3.1 endpoint factory: %w", err) - } - pgFactory, err := creproxy.NewProxyPeerGroupFactory(d.cfg.ProxyAddress) - if err != nil { - _ = endpointFactory.Close() - _ = endpoint2Factory.Close() - return nil, fmt.Errorf("failed to create proxy peer group factory: %w", err) - } - - d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.cfg.ProxyAddress, "peerID", peerID.String()) - - return &Factories{ - OCR2Endpoint: endpointFactory, - OCR3_1Endpoint: endpoint2Factory, - PeerGroup: pgFactory, - PeerID: peerID, - closer: multiCloser{endpointFactory, endpoint2Factory, pgFactory}, - }, nil } // multiCloser closes several io.Closers, returning the first error. diff --git a/libs/standalone/ocr/embed.go b/libs/standalone/ocr/embed.go new file mode 100644 index 000000000..3d7e8d85b --- /dev/null +++ b/libs/standalone/ocr/embed.go @@ -0,0 +1,74 @@ +package ocr + +import ( + "context" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +// embedded is one embedded instance's rage networking: an identity derived from the instance index +// and the in-process transport (see inproc.go and keyring.go). +// +// It is what both Host and Proxy return from ForEmbedding, because embedding erases the difference +// between them. Hosting a peer or delegating to one is a question about a network, and there is no +// network here: the peers an embedded instance talks to are goroutines in the same process, reached +// over channels. Delegating that to a proxy would mean serialising a message so a gRPC connection to +// this process could hand it back. +// +// So this needs no configuration at all: no listen address, no proxy address, no peer ID, and above +// all no keystore password, since the identity is derived rather than unlocked. That is why the +// settings those forms do need are checked when they are resolved rather than tagged `required` - +// an embedded instance would have to be given values it has no use for. +type embedded struct { + lggr commonlogger.Logger + index int +} + +var _ standalone.BootstrapDependency[*Factories] = (*embedded)(nil) + +// Namespace is the same ocr.* the configured forms use, though it names nothing: there is no +// configuration here to root under it. +func (d *embedded) Namespace() string { return "ocr" } + +// Config is nil: there is nothing to configure. Which is the point - an embedded instance cannot be +// told a listen address, a proxy address, a peer ID or a keystore password, so `embed` does not +// offer them. +func (d *embedded) Config() any { return nil } + +func (d *embedded) Dependencies() []standalone.BootstrapCommand { + // No database: the identity is derived, and there are no announcements to store when every peer + // is in this process. + return []standalone.BootstrapCommand{} +} + +// ForEmbedding returns the dependency of instance i, so an already-embedded dependency embedded +// again is that instance's rather than a nesting of them. +func (d *embedded) ForEmbedding(i int) standalone.BootstrapDependency[*Factories] { + return &embedded{lggr: d.lggr, index: i} +} + +func (d *embedded) Get(context.Context, standalone.CommonConfig) (*Factories, error) { + keyring, err := DeterministicKeyring(d.index) + if err != nil { + return nil, err + } + peerID := ragetypes.PeerIDFromKeyring(keyring) + + d.lggr.Infow("Using in-process rage networking", + "instance", d.index, + "peerID", peerID.String(), + ) + + // Nothing here is closed: the endpoints and peer groups are closed by whoever created them, and + // the transport itself holds no resource beyond the maps they register in. + return &Factories{ + OCR2Endpoint: ocr2Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, + OCR3_1Endpoint: ocr31Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, + PeerGroup: inprocPeerGroupFactory{net: embedNetwork, peerID: peerID.String()}, + PeerID: peerID, + }, nil +} diff --git a/libs/standalone/ocr/host.go b/libs/standalone/ocr/host.go new file mode 100644 index 000000000..dc63b7e2a --- /dev/null +++ b/libs/standalone/ocr/host.go @@ -0,0 +1,161 @@ +package ocr + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/jmoiron/sqlx" + "github.com/prometheus/client_golang/prometheus" + + "github.com/smartcontractkit/libocr/networking" + "github.com/smartcontractkit/libocr/networking/rageping" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/config" + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +// Config is the configuration of a locally hosted libocr peer: the peer's own settings +// (ocrcommon.Config, shared with anything else that runs one) plus the keystore password. +// +// The password is this struct's rather than the shared one's because needing it is this +// framework's choice, not the peer's: the peer needs an identity, and this process happens to get +// one by unlocking the keystore in the database it shares with the node it fronts. An embedded +// instance derives its identity instead and resolves a dependency with no settings at all (see +// ForEmbedding), so neither this nor the listen addresses is `validate:"required"` - a rule that +// only holds for the form a real deployment resolves is checked when that form is resolved, and +// spelled out in the usage text since the generated docs can only show what a tag says. +type Config struct { + // Embedded as a pointer so this struct adds to the shared peer settings rather than copying + // them; inline so its fields sit alongside keystore-password under ocr.* instead of nesting. + // + //nolint:revive // struct-tag: "inline" is pkg/config/flags' squash option (Options.SquashTagOption), not a go-toml one + *ocrcommon.Config `toml:",inline"` + + // KeystorePassword unlocks the keystore holding the P2P identity the peer announces under. + // Typed as a SecretString so it redacts itself in logs, docs and generated example configs. + KeystorePassword config.SecretString `usage:"password for the node keystore holding the shared P2P identity; required unless the identity is derived, as it is under embed"` +} + +// Host returns a standalone.BootstrapDependency that resolves the libocr Factories from a peer +// this process hosts itself. It wraps the database dependency, which it uses to load the node's +// P2P identity and to back the OCR discoverer table. discovererTable is the migration-created +// table holding p2p announcements. +// +// An embedded instance resolves neither: see ForEmbedding. +func Host(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*Factories] { + // Wrap in OnceBootstrapper so Get (which creates the peer) runs at most once even if several + // services resolve this dependency. + return standalone.OnceBootstrapper[*Factories](&hostDependency{ + lggr: lggr, + db: db, + discovererTable: discovererTable, + cfg: Config{Config: defaultPeerConfig()}, + }) +} + +type hostDependency struct { + lggr commonlogger.Logger + db standalone.BootstrapDependency[*sql.DB] + discovererTable string + + cfg Config +} + +var _ standalone.BootstrapDependency[*Factories] = (*hostDependency)(nil) + +// Namespace groups the libocr networking settings under ocr.* (--ocr.listen-addresses, +// CRE_OCR_LISTEN_ADDRESSES). +func (d *hostDependency) Namespace() string { return "ocr" } + +func (d *hostDependency) Config() any { return &d.cfg } + +func (d *hostDependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{d.db} +} + +// ForEmbedding returns the in-process form, which hosts no peer at all: an embedded instance's peers +// are goroutines beside it, so there is nothing for a rage peer to listen on, announce to or +// discover. None of this dependency's settings survive into it - see embedded. +func (d *hostDependency) ForEmbedding(i int) standalone.BootstrapDependency[*Factories] { + return &embedded{lggr: d.lggr, index: i} +} + +func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Factories, error) { + if len(d.cfg.ListenAddresses) == 0 { + return nil, errors.New("--ocr.listen-addresses is required to host a peer") + } + + ds, keyring, peerID, err := d.keystoreIdentity(ctx, cc) + if err != nil { + return nil, err + } + return d.localFactories(ds, keyring, peerID) +} + +// keystoreIdentity loads the node's P2P identity from the keystore in the database this process +// shares with the node, so this process is the same peer as the node it fronts. The returned +// sqlx.DB is the same connection, which localFactories also needs for the discoverer table. +// +// Only a process hosting a peer does this: it is the one that needs the private key, and so the +// only one that is given the password to it. +func (d *hostDependency) keystoreIdentity(ctx context.Context, cc standalone.CommonConfig) (*sqlx.DB, ragetypes.PeerKeyring, ragetypes.PeerID, error) { + if d.cfg.KeystorePassword == "" { + return nil, nil, ragetypes.PeerID{}, errors.New("--ocr.keystore-password is required to unlock the node's P2P identity") + } + + sqlDB, err := d.db.Get(ctx, cc) + if err != nil { + return nil, nil, ragetypes.PeerID{}, fmt.Errorf("failed to get database: %w", err) + } + ds := sqlx.NewDb(sqlDB, "pgx") + + keyring, err := loadPeerKeyring(ctx, ds, string(d.cfg.KeystorePassword)) + if err != nil { + return nil, nil, ragetypes.PeerID{}, err + } + return ds, keyring, ragetypes.PeerIDFromKeyring(keyring), nil +} + +// localFactories builds a real libocr peer and exposes its factories. +func (d *hostDependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*Factories, error) { + discovererDB := ocrcommon.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) + + d.lggr.Infow("Creating local p2p peer", + "peerID", peerID.String(), + "listenAddresses", d.cfg.ListenAddresses, + "announceAddresses", d.cfg.AnnounceAddresses, + ) + + peer, err := networking.NewPeer(networking.PeerConfig{ + PeerKeyring: keyring, + Logger: commonlogger.NewOCRWrapper(d.lggr, false, func(string) {}), + V2ListenAddresses: d.cfg.ListenAddresses, + V2AnnounceAddresses: d.cfg.AnnounceAddresses, + V2DeltaReconcile: d.cfg.DeltaReconcile.Duration(), + V2DeltaDial: d.cfg.DeltaDial.Duration(), + V2DiscovererDatabase: discovererDB, + V2EndpointConfig: networking.EndpointConfigV2{ + IncomingMessageBufferSize: d.cfg.IncomingBufferSize, + OutgoingMessageBufferSize: d.cfg.OutgoingBufferSize, + }, + MetricsRegisterer: prometheus.DefaultRegisterer, + LatencyMetricsServiceConfigs: rageping.DefaultConfigs(), + }) + if err != nil { + return nil, fmt.Errorf("failed to create rage peer: %w", err) + } + + return &Factories{ + OCR2Endpoint: peer.OCR2BinaryNetworkEndpointFactory(), + OCR3_1Endpoint: peer.OCR3_1BinaryNetworkEndpointFactory(), + PeerGroup: newNetworkingPeerGroupFactory(peer.PeerGroupFactory()), + PeerID: peerID, + closer: peer, + }, nil +} diff --git a/libs/standalone/ocr/host_test.go b/libs/standalone/ocr/host_test.go new file mode 100644 index 000000000..1942053d0 --- /dev/null +++ b/libs/standalone/ocr/host_test.go @@ -0,0 +1,45 @@ +package ocr + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +func TestHostForEmbedding(t *testing.T) { + // A nil database dependency: the adapted instances resolve without one, which is the property + // under test - an embedded instance neither unlocks a keystore nor stores announcements. + template := Host(logger.Test(t), nil, "announcements") + + first := template.ForEmbedding(0) + second := template.ForEmbedding(1) + + t.Run("each instance derives its own identity", func(t *testing.T) { + ctx := t.Context() + + firstFactories, err := first.Get(ctx, standalone.CommonConfig{}) + require.NoError(t, err) + secondFactories, err := second.Get(ctx, standalone.CommonConfig{}) + require.NoError(t, err) + + assert.Equal(t, mustPeerID(t, 0), firstFactories.PeerID) + assert.Equal(t, mustPeerID(t, 1), secondFactories.PeerID) + // The factories announce the same identity to libocr as the one the registry sees. + assert.Equal(t, firstFactories.PeerID.String(), firstFactories.OCR2Endpoint.PeerID()) + assert.Equal(t, secondFactories.PeerID.String(), secondFactories.OCR3_1Endpoint.PeerID()) + }) + + t.Run("hosting a peer needs an address to listen on", func(t *testing.T) { + // The dependency as a single instance resolves it, and a fresh one since the embedded forms + // above have already resolved and cached their factories. + _, err := Host(logger.Test(t), nil, "announcements").Get(t.Context(), standalone.CommonConfig{}) + // Cannot be a `validate` tag: an embedded instance resolves a different dependency, which + // needs no address at all. + require.ErrorContains(t, err, "--ocr.listen-addresses is required") + }) +} diff --git a/libs/standalone/ocr/inproc.go b/libs/standalone/ocr/inproc.go new file mode 100644 index 000000000..a4bd4e1a2 --- /dev/null +++ b/libs/standalone/ocr/inproc.go @@ -0,0 +1,485 @@ +package ocr + +import ( + "fmt" + "math" + "slices" + "sync" + + "github.com/smartcontractkit/libocr/commontypes" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// This file implements the rage networking factories as in-process message passing, for embed +// mode: several instances of a binary running in one process, talking to each other over channels +// instead of over the network. +// +// libocr exports no portless transport, so there is one here. Every instance holds a peer +// identity as usual (a derived one, see keyring.go) and endpoints are still created per config +// digest from the peer IDs in the OCR config, so what runs on top cannot tell the difference - +// but nothing listens, dials, announces or discovers, and no port is needed. Which is the point: +// instances of one process have nothing to gain from a loopback socket between them, and needing +// a free port per instance is exactly the kind of setup embedding exists to avoid. +// +// What is deliberately dropped: the rate limits and message length limits libocr passes in (the +// send side is a channel, and enforcing a byte budget on it would only slow down a test), the +// bootstrapper locators (there are no addresses to dial), and delivery to peers that have not +// created their endpoint yet or whose mailbox is full (dropped, as ragep2p drops when a peer's +// buffer is full). Payloads are copied on send, so a caller reusing its buffer cannot corrupt a +// message in flight the way it could not over a real socket. + +// network is the in-process transport shared by every instance in the process: a registry of +// mailboxes keyed by config digest and peer ID, plus the streams peer groups open between them. +type network struct { + mu sync.Mutex + ocr2 map[endpointKey]*ocr2Endpoint + ocr31 map[endpointKey]*binaryNetworkEndpoint2 + streams map[streamKey]*peerGroupStream +} + +// embedNetwork is the process's in-process network. It is package-level for the same reason +// prometheus.DefaultRegisterer is: there is exactly one process, so there is exactly one network +// inside it, and every instance resolving its factories has to find the same one. Tests that want +// an isolated network construct one with newNetwork. +var embedNetwork = newNetwork() + +func newNetwork() *network { + return &network{ + ocr2: map[endpointKey]*ocr2Endpoint{}, + ocr31: map[endpointKey]*binaryNetworkEndpoint2{}, + streams: map[streamKey]*peerGroupStream{}, + } +} + +// endpointKey identifies one peer's endpoint for one OCR instance. The digest is part of the key +// because a peer runs one endpoint per config digest, as it does over rage networking. +type endpointKey struct { + digest ocr2types.ConfigDigest + peerID string +} + +// streamKey identifies one direction of one peer group stream: the stream owner is where messages +// arrive, and remote is who sends them. +type streamKey struct { + digest [32]byte + name string + owner string + remote string +} + +// senderIndex is the oracle ID the receiver knows sender by. The two ends of a link index oracles +// by their own copy of the OCR config's peer IDs, so a message carries the sender's position in +// the receiver's list, not in the sender's. +func senderIndex(receiverPeerIDs []string, senderPeerID string) (commontypes.OracleID, bool) { + i := slices.Index(receiverPeerIDs, senderPeerID) + // An oracle ID is a uint8, so an oracle beyond the 256th cannot be named in a message at all - + // there is no such DON, and truncating the index would attribute the message to another oracle. + if i < 0 || i > math.MaxUint8 { + return 0, false + } + return commontypes.OracleID(i), true +} + +// bufferSize returns size, or fallback when size is not positive, so an unset buffer setting +// still yields a usable mailbox. +func bufferSize(size, fallback int) int { + if size > 0 { + return size + } + return fallback +} + +// defaultBufferSize is used when neither the caller nor the config asks for a size. +const defaultBufferSize = 100 + +// ocr2Factory creates in-process OCR2 endpoints for one peer. +type ocr2Factory struct { + net *network + peerID string + bufferSize int +} + +var _ ocr2types.BinaryNetworkEndpointFactory = ocr2Factory{} + +func (f ocr2Factory) PeerID() string { return f.peerID } + +func (f ocr2Factory) NewEndpoint( + digest ocr2types.ConfigDigest, + peerIDs []string, + _ []commontypes.BootstrapperLocator, + _ int, + _ ocr2types.BinaryNetworkEndpointLimits, +) (commontypes.BinaryNetworkEndpoint, error) { + if !slices.Contains(peerIDs, f.peerID) { + return nil, fmt.Errorf("peer %s is not one of the oracles of config digest %s", f.peerID, digest) + } + + e := &ocr2Endpoint{ + net: f.net, + key: endpointKey{digest: digest, peerID: f.peerID}, + peerIDs: slices.Clone(peerIDs), + in: make(chan commontypes.BinaryMessageWithSender, f.bufferSize), + closed: make(chan struct{}), + } + + f.net.mu.Lock() + defer f.net.mu.Unlock() + if _, exists := f.net.ocr2[e.key]; exists { + return nil, fmt.Errorf("peer %s already has an OCR2 endpoint for config digest %s", f.peerID, digest) + } + f.net.ocr2[e.key] = e + return e, nil +} + +// ocr2Endpoint is a commontypes.BinaryNetworkEndpoint delivering to other endpoints registered on +// the same network under the same config digest. +type ocr2Endpoint struct { + net *network + key endpointKey + peerIDs []string + in chan commontypes.BinaryMessageWithSender + + closeOnce sync.Once + closed chan struct{} +} + +var _ commontypes.BinaryNetworkEndpoint = (*ocr2Endpoint)(nil) + +func (e *ocr2Endpoint) Start() error { return nil } + +func (e *ocr2Endpoint) SendTo(payload []byte, to commontypes.OracleID) { + if int(to) >= len(e.peerIDs) { + return + } + e.deliver(payload, e.peerIDs[to]) +} + +func (e *ocr2Endpoint) Broadcast(payload []byte) { + for _, peerID := range e.peerIDs { + if peerID == e.key.peerID { + continue + } + e.deliver(payload, peerID) + } +} + +// deliver hands a copy of payload to peerID's mailbox, dropping it if that peer has no endpoint +// for this config digest or its mailbox is full. Never blocks: libocr requires SendTo and +// Broadcast not to. +func (e *ocr2Endpoint) deliver(payload []byte, peerID string) { + e.net.mu.Lock() + peer := e.net.ocr2[endpointKey{digest: e.key.digest, peerID: peerID}] + e.net.mu.Unlock() + if peer == nil { + return + } + + sender, ok := senderIndex(peer.peerIDs, e.key.peerID) + if !ok { + return + } + + msg := commontypes.BinaryMessageWithSender{Msg: slices.Clone(payload), Sender: sender} + select { + case <-peer.closed: + case peer.in <- msg: + default: + } +} + +func (e *ocr2Endpoint) Receive() <-chan commontypes.BinaryMessageWithSender { return e.in } + +// Close unregisters the endpoint. The receive channel is left open: libocr may still be selecting +// on it, and a closed channel would hand it an endless stream of zero-valued messages. +func (e *ocr2Endpoint) Close() error { + e.closeOnce.Do(func() { + close(e.closed) + e.net.mu.Lock() + delete(e.net.ocr2, e.key) + e.net.mu.Unlock() + }) + return nil +} + +// ocr31Factory creates in-process OCR3.1 endpoints for one peer. +type ocr31Factory struct { + net *network + peerID string + bufferSize int +} + +var _ ocr2types.BinaryNetworkEndpoint2Factory = ocr31Factory{} + +func (f ocr31Factory) PeerID() string { return f.peerID } + +func (f ocr31Factory) NewEndpoint( + digest ocr2types.ConfigDigest, + peerIDs []string, + _ []commontypes.BootstrapperLocator, + defaultPriorityConfig ocr2types.BinaryNetworkEndpoint2Config, + _ ocr2types.BinaryNetworkEndpoint2Config, +) (ocr2types.BinaryNetworkEndpoint2, error) { + if !slices.Contains(peerIDs, f.peerID) { + return nil, fmt.Errorf("peer %s is not one of the oracles of config digest %s", f.peerID, digest) + } + + // One mailbox carries both priorities, each message keeping the priority it was sent with, so + // only the default priority config's buffer override is meaningful here. + size := f.bufferSize + if override := defaultPriorityConfig.OverrideIncomingMessageBufferSize; override != nil { + size = bufferSize(*override, size) + } + + e := &binaryNetworkEndpoint2{ + net: f.net, + key: endpointKey{digest: digest, peerID: f.peerID}, + peerIDs: slices.Clone(peerIDs), + in: make(chan ocr2types.InboundBinaryMessageWithSender, size), + closed: make(chan struct{}), + } + + f.net.mu.Lock() + defer f.net.mu.Unlock() + if _, exists := f.net.ocr31[e.key]; exists { + return nil, fmt.Errorf("peer %s already has an OCR3.1 endpoint for config digest %s", f.peerID, digest) + } + f.net.ocr31[e.key] = e + return e, nil +} + +// binaryNetworkEndpoint2 is an ocr2types.BinaryNetworkEndpoint2 over the in-process network. +type binaryNetworkEndpoint2 struct { + net *network + key endpointKey + peerIDs []string + in chan ocr2types.InboundBinaryMessageWithSender + + closeOnce sync.Once + closed chan struct{} +} + +var _ ocr2types.BinaryNetworkEndpoint2 = (*binaryNetworkEndpoint2)(nil) + +func (e *binaryNetworkEndpoint2) SendTo(msg ocr2types.OutboundBinaryMessage, to commontypes.OracleID) { + if int(to) >= len(e.peerIDs) { + return + } + e.deliver(msg, e.peerIDs[to]) +} + +func (e *binaryNetworkEndpoint2) Broadcast(msg ocr2types.OutboundBinaryMessage) { + for _, peerID := range e.peerIDs { + if peerID == e.key.peerID { + continue + } + e.deliver(msg, peerID) + } +} + +func (e *binaryNetworkEndpoint2) deliver(msg ocr2types.OutboundBinaryMessage, peerID string) { + e.net.mu.Lock() + peer := e.net.ocr31[endpointKey{digest: e.key.digest, peerID: peerID}] + e.net.mu.Unlock() + if peer == nil { + return + } + + sender, ok := senderIndex(peer.peerIDs, e.key.peerID) + if !ok { + return + } + + inbound, ok := inboundMessage(msg) + if !ok { + return + } + + select { + case <-peer.closed: + case peer.in <- ocr2types.InboundBinaryMessageWithSender{InboundBinaryMessage: inbound, Sender: sender}: + default: + } +} + +func (e *binaryNetworkEndpoint2) Receive() <-chan ocr2types.InboundBinaryMessageWithSender { + return e.in +} + +func (e *binaryNetworkEndpoint2) Close() error { + e.closeOnce.Do(func() { + close(e.closed) + e.net.mu.Lock() + delete(e.net.ocr31, e.key) + e.net.mu.Unlock() + }) + return nil +} + +// inboundMessage converts a message as the sender wrote it into the message the receiver reads, +// which is what a transport does: the wire has one representation, and each side sees its own view +// of it. A request arrives with a handle the receiver answers through; ok is false for a message +// type this transport does not know, which is dropped rather than delivered as something else. +func inboundMessage(msg ocr2types.OutboundBinaryMessage) (ocr2types.InboundBinaryMessage, bool) { + switch m := msg.(type) { + case ocr2types.OutboundBinaryMessagePlain: + return ocr2types.InboundBinaryMessagePlain{ + Payload: slices.Clone(m.Payload), + Priority: m.Priority, + }, true + case ocr2types.OutboundBinaryMessageRequest: + return ocr2types.InboundBinaryMessageRequest{ + RequestHandle: requestHandle{priority: m.Priority}, + Payload: slices.Clone(m.Payload), + Priority: m.Priority, + }, true + case ocr2types.OutboundBinaryMessageResponse: + return ocr2types.InboundBinaryMessageResponse{ + Payload: slices.Clone(m.Payload), + Priority: m.Priority, + }, true + default: + return nil, false + } +} + +// requestHandle is what a receiver answers an inbound request through. It carries only the +// request's priority: with a ragep2p backend a response has to be sent at the priority its +// request used or it is dropped, and libocr's own endpoints keep that invariant by building the +// response from the handle. Where the response goes is not its business - the caller passes the +// requester to SendTo, as it does for any other message. +type requestHandle struct { + priority ocr2types.BinaryMessageOutboundPriority +} + +var _ ocr2types.RequestHandle = requestHandle{} + +func (h requestHandle) MakeResponse(payload []byte) ocr2types.OutboundBinaryMessageResponse { + return ocr2types.MustMakeOutboundBinaryMessageResponse(h, payload, h.priority) +} + +// inprocPeerGroupFactory creates in-process DON-to-DON peer groups for one peer. +type inprocPeerGroupFactory struct { + net *network + peerID string +} + +var _ creproxy.PeerGroupFactory = inprocPeerGroupFactory{} + +func (f inprocPeerGroupFactory) NewPeerGroup(digest [32]byte, peerIDs []string, _ []creproxy.BootstrapperInfo) (creproxy.PeerGroup, error) { + if !slices.Contains(peerIDs, f.peerID) { + return nil, fmt.Errorf("peer %s is not a member of peer group %x", f.peerID, digest) + } + return &inprocPeerGroup{net: f.net, digest: digest, peerID: f.peerID, peerIDs: slices.Clone(peerIDs)}, nil +} + +// inprocPeerGroup hands out streams to other members of the group. Its streams are closed with +// it, per the creproxy.PeerGroup contract. +type inprocPeerGroup struct { + net *network + digest [32]byte + peerID string + peerIDs []string + + mu sync.Mutex + streams []*peerGroupStream + closed bool +} + +var _ creproxy.PeerGroup = (*inprocPeerGroup)(nil) + +func (g *inprocPeerGroup) NewStream(remotePeerID string, args creproxy.StreamArgs) (creproxy.PeerGroupStream, error) { + if !slices.Contains(g.peerIDs, remotePeerID) { + return nil, fmt.Errorf("peer %s is not a member of peer group %x", remotePeerID, g.digest) + } + + s := &peerGroupStream{ + net: g.net, + // Keyed by where messages arrive, so the two ends of the same named stream are each + // other's mirror: what this end sends is looked up under the remote's key. + key: streamKey{digest: g.digest, name: args.StreamName, owner: g.peerID, remote: remotePeerID}, + in: make(chan []byte, bufferSize(args.IncomingBufferSize, defaultBufferSize)), + closed: make(chan struct{}), + } + + g.mu.Lock() + if g.closed { + g.mu.Unlock() + return nil, fmt.Errorf("peer group %x is closed", g.digest) + } + g.streams = append(g.streams, s) + g.mu.Unlock() + + g.net.mu.Lock() + defer g.net.mu.Unlock() + if _, exists := g.net.streams[s.key]; exists { + return nil, fmt.Errorf("stream %q to peer %s already exists in peer group %x", args.StreamName, remotePeerID, g.digest) + } + g.net.streams[s.key] = s + return s, nil +} + +func (g *inprocPeerGroup) Close() error { + g.mu.Lock() + if g.closed { + g.mu.Unlock() + return nil + } + g.closed = true + streams := g.streams + g.streams = nil + g.mu.Unlock() + + var err error + for _, s := range streams { + if cerr := s.Close(); cerr != nil && err == nil { + err = cerr + } + } + return err +} + +// peerGroupStream is one end of a named bidirectional stream between two group members. +type peerGroupStream struct { + net *network + key streamKey + in chan []byte + + closeOnce sync.Once + closed chan struct{} +} + +var _ creproxy.PeerGroupStream = (*peerGroupStream)(nil) + +// SendMessage delivers to the stream the remote end opened back to us, dropping the message if it +// has not opened one yet or its buffer is full - the same outcome as sending on a rage stream to a +// peer that is not connected. +func (s *peerGroupStream) SendMessage(data []byte) { + mirror := streamKey{digest: s.key.digest, name: s.key.name, owner: s.key.remote, remote: s.key.owner} + + s.net.mu.Lock() + peer := s.net.streams[mirror] + s.net.mu.Unlock() + if peer == nil { + return + } + + select { + case <-peer.closed: + case peer.in <- slices.Clone(data): + default: + } +} + +func (s *peerGroupStream) ReceiveMessages() <-chan []byte { return s.in } + +func (s *peerGroupStream) Close() error { + s.closeOnce.Do(func() { + close(s.closed) + s.net.mu.Lock() + delete(s.net.streams, s.key) + s.net.mu.Unlock() + }) + return nil +} diff --git a/libs/standalone/ocr/inproc_test.go b/libs/standalone/ocr/inproc_test.go new file mode 100644 index 000000000..97af5db07 --- /dev/null +++ b/libs/standalone/ocr/inproc_test.go @@ -0,0 +1,255 @@ +package ocr + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/libocr/commontypes" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// receiveTimeout is how long a test waits for a message that should already be on its way. The +// transport is channels in one process, so anything slower than this is a bug rather than a slow +// machine. +const receiveTimeout = 5 * time.Second + +var testDigest = ocr2types.ConfigDigest{1, 2, 3} + +// testPeerIDs returns the derived peer IDs of the first count instances, in oracle order. +func testPeerIDs(t *testing.T, count int) []string { + t.Helper() + peerIDs := make([]string, count) + for i := range count { + peerID, err := DeterministicPeerID(i) + require.NoError(t, err) + peerIDs[i] = peerID.String() + } + return peerIDs +} + +func TestInprocOCR2Endpoints(t *testing.T) { + net := newNetwork() + peerIDs := testPeerIDs(t, 3) + + endpoints := make([]commontypes.BinaryNetworkEndpoint, len(peerIDs)) + for i, peerID := range peerIDs { + factory := ocr2Factory{net: net, peerID: peerID, bufferSize: 10} + require.Equal(t, peerID, factory.PeerID()) + + e, err := factory.NewEndpoint(testDigest, peerIDs, nil, 1, ocr2types.BinaryNetworkEndpointLimits{}) + require.NoError(t, err) + require.NoError(t, e.Start()) + t.Cleanup(func() { assert.NoError(t, e.Close()) }) + endpoints[i] = e + } + + t.Run("SendTo reaches only the addressed oracle", func(t *testing.T) { + endpoints[2].SendTo([]byte("for oracle 0"), 0) + + msg := receive(t, endpoints[0].Receive()) + assert.Equal(t, []byte("for oracle 0"), msg.Msg) + assert.Equal(t, commontypes.OracleID(2), msg.Sender) + assertNothingReceived(t, endpoints[1].Receive()) + }) + + t.Run("Broadcast reaches every other oracle but not the sender", func(t *testing.T) { + endpoints[0].Broadcast([]byte("to everyone")) + + for _, i := range []int{1, 2} { + msg := receive(t, endpoints[i].Receive()) + assert.Equal(t, []byte("to everyone"), msg.Msg) + assert.Equal(t, commontypes.OracleID(0), msg.Sender) + } + assertNothingReceived(t, endpoints[0].Receive()) + }) + + t.Run("payloads are copied, so a reused buffer cannot rewrite a sent message", func(t *testing.T) { + payload := []byte("original") + endpoints[1].SendTo(payload, 0) + copy(payload, "OVERWROTE") + + assert.Equal(t, []byte("original"), receive(t, endpoints[0].Receive()).Msg) + }) + + t.Run("a closed endpoint receives nothing more", func(t *testing.T) { + factory := ocr2Factory{net: net, peerID: peerIDs[0], bufferSize: 10} + // The endpoint registered in the parent test still holds this peer's slot. + _, err := factory.NewEndpoint(testDigest, peerIDs, nil, 1, ocr2types.BinaryNetworkEndpointLimits{}) + require.ErrorContains(t, err, "already has an OCR2 endpoint") + + other := ocr2Factory{net: net, peerID: peerIDs[1], bufferSize: 10} + e, err := other.NewEndpoint(ocr2types.ConfigDigest{9}, peerIDs, nil, 1, ocr2types.BinaryNetworkEndpointLimits{}) + require.NoError(t, err) + require.NoError(t, e.Close()) + + // Nothing to deliver to under that digest now: this must drop rather than block or panic. + endpoints[0].Broadcast([]byte("dropped")) + assertNothingReceived(t, e.Receive()) + }) + + t.Run("a peer outside the oracle set is refused", func(t *testing.T) { + stranger, err := DeterministicPeerID(len(peerIDs) + 1) + require.NoError(t, err) + + factory := ocr2Factory{net: net, peerID: stranger.String(), bufferSize: 10} + _, err = factory.NewEndpoint(testDigest, peerIDs, nil, 1, ocr2types.BinaryNetworkEndpointLimits{}) + require.ErrorContains(t, err, "is not one of the oracles") + }) +} + +func TestInprocOCR31Endpoints(t *testing.T) { + net := newNetwork() + peerIDs := testPeerIDs(t, 2) + + endpoints := make([]ocr2types.BinaryNetworkEndpoint2, len(peerIDs)) + for i, peerID := range peerIDs { + factory := ocr31Factory{net: net, peerID: peerID, bufferSize: 10} + e, err := factory.NewEndpoint(testDigest, peerIDs, nil, + ocr2types.BinaryNetworkEndpoint2Config{}, ocr2types.BinaryNetworkEndpoint2Config{}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, e.Close()) }) + endpoints[i] = e + } + + t.Run("a plain message keeps its payload and priority", func(t *testing.T) { + endpoints[0].SendTo(ocr2types.OutboundBinaryMessagePlain{ + Payload: []byte("plain"), + Priority: ocr2types.BinaryMessagePriorityLow, + }, 1) + + msg := receive(t, endpoints[1].Receive()) + assert.Equal(t, commontypes.OracleID(0), msg.Sender) + plain, ok := msg.InboundBinaryMessage.(ocr2types.InboundBinaryMessagePlain) + require.True(t, ok, "expected a plain message, got %T", msg.InboundBinaryMessage) + assert.Equal(t, []byte("plain"), plain.Payload) + assert.Equal(t, ocr2types.BinaryMessagePriorityLow, plain.Priority) + }) + + t.Run("a request is answered through its handle, at the request's priority", func(t *testing.T) { + endpoints[0].SendTo(ocr2types.OutboundBinaryMessageRequest{ + Payload: []byte("question"), + Priority: ocr2types.BinaryMessagePriorityDefault, + }, 1) + + inbound := receive(t, endpoints[1].Receive()) + request, ok := inbound.InboundBinaryMessage.(ocr2types.InboundBinaryMessageRequest) + require.True(t, ok, "expected a request, got %T", inbound.InboundBinaryMessage) + assert.Equal(t, []byte("question"), request.Payload) + require.NotNil(t, request.RequestHandle) + + endpoints[1].SendTo(request.RequestHandle.MakeResponse([]byte("answer")), inbound.Sender) + + reply := receive(t, endpoints[0].Receive()) + assert.Equal(t, commontypes.OracleID(1), reply.Sender) + response, ok := reply.InboundBinaryMessage.(ocr2types.InboundBinaryMessageResponse) + require.True(t, ok, "expected a response, got %T", reply.InboundBinaryMessage) + assert.Equal(t, []byte("answer"), response.Payload) + // A ragep2p backend drops a response whose priority differs from its request's, so the + // handle has to preserve it. + assert.Equal(t, ocr2types.BinaryMessagePriorityDefault, response.Priority) + }) + + t.Run("the incoming buffer override sizes the mailbox", func(t *testing.T) { + size := 1 + factory := ocr31Factory{net: net, peerID: peerIDs[0], bufferSize: 10} + e, err := factory.NewEndpoint(ocr2types.ConfigDigest{7}, peerIDs, nil, + ocr2types.BinaryNetworkEndpoint2Config{OverrideIncomingMessageBufferSize: &size}, + ocr2types.BinaryNetworkEndpoint2Config{}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, e.Close()) }) + + assert.Equal(t, size, cap(e.(*binaryNetworkEndpoint2).in)) + }) +} + +func TestInprocPeerGroupStreams(t *testing.T) { + net := newNetwork() + peerIDs := testPeerIDs(t, 2) + var digest [32]byte + digest[0] = 42 + + groups := make([]creproxy.PeerGroup, len(peerIDs)) + for i, peerID := range peerIDs { + g, err := inprocPeerGroupFactory{net: net, peerID: peerID}.NewPeerGroup(digest, peerIDs, nil) + require.NoError(t, err) + groups[i] = g + } + + args := creproxy.StreamArgs{StreamName: "stream", IncomingBufferSize: 10, OutgoingBufferSize: 10} + streamFrom0, err := groups[0].NewStream(peerIDs[1], args) + require.NoError(t, err) + streamFrom1, err := groups[1].NewStream(peerIDs[0], args) + require.NoError(t, err) + + t.Run("messages flow both ways between the two ends", func(t *testing.T) { + streamFrom0.SendMessage([]byte("ping")) + assert.Equal(t, []byte("ping"), receive(t, streamFrom1.ReceiveMessages())) + + streamFrom1.SendMessage([]byte("pong")) + assert.Equal(t, []byte("pong"), receive(t, streamFrom0.ReceiveMessages())) + }) + + t.Run("a differently named stream is a different stream", func(t *testing.T) { + other, err := groups[0].NewStream(peerIDs[1], creproxy.StreamArgs{StreamName: "other", IncomingBufferSize: 10}) + require.NoError(t, err) + + // Nothing is listening on "other" at the remote end, so this is dropped rather than + // delivered to the stream that happens to connect the same two peers. + other.SendMessage([]byte("nowhere")) + assertNothingReceived(t, streamFrom1.ReceiveMessages()) + }) + + t.Run("a non-member cannot be streamed to", func(t *testing.T) { + stranger, err := DeterministicPeerID(len(peerIDs) + 1) + require.NoError(t, err) + + _, err = groups[0].NewStream(stranger.String(), args) + require.ErrorContains(t, err, "is not a member of peer group") + }) + + t.Run("closing a group closes its streams and stops delivery", func(t *testing.T) { + require.NoError(t, groups[1].Close()) + + streamFrom0.SendMessage([]byte("after close")) + assertNothingReceived(t, streamFrom1.ReceiveMessages()) + + // Closing again is a no-op, as is closing a stream the group already closed. + require.NoError(t, groups[1].Close()) + require.NoError(t, streamFrom1.Close()) + + _, err := groups[1].NewStream(peerIDs[0], args) + require.ErrorContains(t, err, "is closed") + + require.NoError(t, groups[0].Close()) + }) +} + +// receive returns the next value on ch, failing the test if none arrives. +func receive[T any](t *testing.T, ch <-chan T) T { + t.Helper() + select { + case v := <-ch: + return v + case <-time.After(receiveTimeout): + var zero T + t.Fatalf("timed out waiting for a message") + return zero + } +} + +// assertNothingReceived fails if anything is waiting on ch. Nothing sleeps here: delivery is a +// channel send inside the SendTo call that preceded this, so a message that was going to arrive +// has already arrived. +func assertNothingReceived[T any](t *testing.T, ch <-chan T) { + t.Helper() + select { + case v := <-ch: + t.Fatalf("expected no message, got %v", v) + default: + } +} diff --git a/libs/standalone/ocr/keyring.go b/libs/standalone/ocr/keyring.go index 2be0aa214..494310745 100644 --- a/libs/standalone/ocr/keyring.go +++ b/libs/standalone/ocr/keyring.go @@ -3,9 +3,12 @@ package ocr import ( "context" "crypto" + "crypto/ed25519" "crypto/rand" + "crypto/sha256" "errors" "fmt" + "strconv" "github.com/jmoiron/sqlx" @@ -47,8 +50,46 @@ func loadPeerKeyring(ctx context.Context, ds *sqlx.DB, password string) (*peerKe return nil, errors.New("no P2P key found in node key ring") } -// peerKeyring is a ragetypes.PeerKeyring backed by the node's P2P key (a -// crypto.Signer), used in place of the deprecated PeerConfig.PrivKey field. +// deterministicSeedPrefix domain-separates the derived instance keys, so the seeds are specific +// to this framework's embed mode and cannot collide with another scheme deriving keys from an +// index. Changing it changes every derived peer ID. +const deterministicSeedPrefix = "cre/standalone/instance/" + +// DeterministicKeyring returns the P2P identity of instance i of a multi-instance run, derived +// from i rather than read from a keystore. +// +// Embedded instances have no keystore to borrow an identity from - they exist to run a DON on one +// machine without the database and operator setup a real node needs - so their keys come from the +// instance index. Deriving them rather than generating them is what makes that usable: the peer +// IDs of a run are known before it starts, so the OCR configs, registry entries and expectations +// that have to name the DON's members can be computed from the instance count alone (see +// DeterministicPeerID), and they are the same on every run and every machine. +// +// These keys are public by construction. Nothing derived this way is a secret, and nothing that +// matters may be protected by one: embed mode is for local runs and tests. +func DeterministicKeyring(i int) (ragetypes.PeerKeyring, error) { + seed := sha256.Sum256([]byte(deterministicSeedPrefix + strconv.Itoa(i))) + key := ed25519.NewKeyFromSeed(seed[:]) + pub, err := ragetypes.PeerPublicKeyFromGenericPublicKey(key.Public()) + if err != nil { + return nil, fmt.Errorf("failed to derive peer public key for instance %d: %w", i, err) + } + return &peerKeyring{signer: key, publicKey: pub}, nil +} + +// DeterministicPeerID returns the peer ID DeterministicKeyring gives instance i, so a caller +// configuring a DON of embedded instances can name its members without starting them. +func DeterministicPeerID(i int) (ragetypes.PeerID, error) { + keyring, err := DeterministicKeyring(i) + if err != nil { + return ragetypes.PeerID{}, err + } + return ragetypes.PeerIDFromKeyring(keyring), nil +} + +// peerKeyring is a ragetypes.PeerKeyring backed by a P2P key (a crypto.Signer): the node's own, +// loaded from its keystore, or one derived from an instance index. Used in place of the deprecated +// PeerConfig.PrivKey field. type peerKeyring struct { signer crypto.Signer publicKey ragetypes.PeerPublicKey diff --git a/libs/standalone/ocr/keyring_test.go b/libs/standalone/ocr/keyring_test.go new file mode 100644 index 000000000..e24a1dc84 --- /dev/null +++ b/libs/standalone/ocr/keyring_test.go @@ -0,0 +1,58 @@ +package ocr + +import ( + "crypto/ed25519" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" +) + +func TestDeterministicKeyring(t *testing.T) { + t.Run("an instance's identity is the same every time it is derived", func(t *testing.T) { + first, err := DeterministicPeerID(3) + require.NoError(t, err) + again, err := DeterministicPeerID(3) + require.NoError(t, err) + + // The whole point: a caller can name the members of an embedded DON - in an OCR config, a + // registry entry, a test expectation - before the instances exist, and get the same answer + // on the next run and on another machine. + assert.Equal(t, first, again) + }) + + t.Run("instances have different identities", func(t *testing.T) { + seen := map[ragetypes.PeerID]int{} + for i := range 8 { + peerID, err := DeterministicPeerID(i) + require.NoError(t, err) + if previous, exists := seen[peerID]; exists { + t.Fatalf("instances %d and %d derived the same peer ID %s", previous, i, peerID) + } + seen[peerID] = i + } + }) + + t.Run("the derived key signs as the peer ID it announces", func(t *testing.T) { + keyring, err := DeterministicKeyring(1) + require.NoError(t, err) + + msg := []byte("signed by instance 1") + sig, err := keyring.Sign(msg) + require.NoError(t, err) + + pub := keyring.PublicKey() + assert.True(t, ed25519.Verify(pub[:], msg, sig), + "signature does not verify against the keyring's own public key") + assert.Equal(t, ragetypes.PeerIDFromKeyring(keyring), mustPeerID(t, 1)) + }) +} + +func mustPeerID(t *testing.T, i int) ragetypes.PeerID { + t.Helper() + peerID, err := DeterministicPeerID(i) + require.NoError(t, err) + return peerID +} diff --git a/libs/standalone/ocr/proxy.go b/libs/standalone/ocr/proxy.go new file mode 100644 index 000000000..b4cc2610c --- /dev/null +++ b/libs/standalone/ocr/proxy.go @@ -0,0 +1,115 @@ +package ocr + +import ( + "context" + "errors" + "fmt" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +// ProxyConfig is the configuration of a process that delegates its rage networking to a p2p proxy +// hosting the peer, rather than hosting one itself: the address to reach that proxy at, and the +// peer ID it hosts. +// +// The peer ID is configured directly rather than looked up. A delegating process does need to know +// it - libocr compares it against the peer IDs in the OCR config, and the on-chain +// CapabilitiesRegistry keys node records by it - but it is a public value, and the only way to +// obtain it from the keystore is to hold the password that unlocks the keystore's private keys. +// Handing that password to every process that merely wants to know its own name would spread a +// secret across the deployment to no end; the process hosting the peer already has it, and is the +// only one that needs it. +// +// Neither setting is `validate:"required"`: an embedded instance resolves a dependency with no +// settings at all (see ForEmbedding), having no proxy to reach and a derived identity, so the rules +// are checked when this form is resolved instead. +type ProxyConfig struct { + ProxyAddress string `usage:"gRPC address of the p2p proxy this process delegates rage networking to; required outside embed"` + + // PeerID is decoded from its text form by the flags package, since ragetypes.PeerID + // unmarshals text itself - so it is validated as a peer ID when the configuration is decoded + // rather than wherever it is first used. + PeerID ragetypes.PeerID `usage:"this node's rage p2p peer ID, hosted on its behalf by the proxy; required outside embed" example:"'12D3KooWKh28EhBVfiiFh39w3zqtBxzYJhmGfBZNmoL4tRjMWSor'"` +} + +// Proxy returns a standalone.BootstrapDependency that resolves the libocr Factories from proxy +// clients: no peer is created here, and rage networking happens in the process at +// --ocr.proxy-address, on behalf of --ocr.peer-id. +// +// It needs no database: everything a delegating process knows about its identity, it is told. +// +// An embedded instance delegates to nothing: see ForEmbedding. +func Proxy(lggr commonlogger.Logger) standalone.BootstrapDependency[*Factories] { + // Wrap in OnceBootstrapper so Get (which dials the proxy) runs at most once even if several + // services resolve this dependency. + return standalone.OnceBootstrapper[*Factories](&proxyDependency{lggr: lggr}) +} + +type proxyDependency struct { + lggr commonlogger.Logger + + cfg ProxyConfig +} + +var _ standalone.BootstrapDependency[*Factories] = (*proxyDependency)(nil) + +// Namespace groups the settings under ocr.* (--ocr.proxy-address, CRE_OCR_PROXY_ADDRESS), the same +// namespace a hosted peer's settings use: a binary is one or the other, so the names never meet. +func (d *proxyDependency) Namespace() string { return "ocr" } + +func (d *proxyDependency) Config() any { return &d.cfg } + +func (d *proxyDependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{} +} + +// ForEmbedding returns the in-process form, the same one a hosted peer embeds to: there is no proxy +// hop between instances of one process, since the peer this would delegate to is a goroutine beside +// it and delegating would mean serialising a message so a gRPC connection to this process could hand +// it back. None of this dependency's settings survive into it - see embedded. +func (d *proxyDependency) ForEmbedding(i int) standalone.BootstrapDependency[*Factories] { + return &embedded{lggr: d.lggr, index: i} +} + +func (d *proxyDependency) Get(context.Context, standalone.CommonConfig) (*Factories, error) { + if d.cfg.ProxyAddress == "" { + return nil, errors.New("--ocr.proxy-address is required to delegate rage networking") + } + if d.cfg.PeerID == (ragetypes.PeerID{}) { + return nil, errors.New("--ocr.peer-id is required to delegate rage networking") + } + peerID := d.cfg.PeerID + + // The raw peer ID is passed to the endpoint factories, as libocr compares it against the peer + // IDs in the OCR config. + endpointFactory, err := creproxy.NewProxyEndpointFactory(peerID.String(), d.cfg.ProxyAddress) + if err != nil { + return nil, fmt.Errorf("failed to create proxy OCR endpoint factory: %w", err) + } + endpoint2Factory, err := creproxy.NewProxyEndpoint2Factory(peerID.String(), d.cfg.ProxyAddress) + if err != nil { + _ = endpointFactory.Close() + return nil, fmt.Errorf("failed to create proxy OCR3.1 endpoint factory: %w", err) + } + pgFactory, err := creproxy.NewProxyPeerGroupFactory(d.cfg.ProxyAddress) + if err != nil { + _ = endpointFactory.Close() + _ = endpoint2Factory.Close() + return nil, fmt.Errorf("failed to create proxy peer group factory: %w", err) + } + + d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.cfg.ProxyAddress, "peerID", peerID.String()) + + return &Factories{ + OCR2Endpoint: endpointFactory, + OCR3_1Endpoint: endpoint2Factory, + PeerGroup: pgFactory, + PeerID: peerID, + closer: multiCloser{endpointFactory, endpoint2Factory, pgFactory}, + }, nil +} diff --git a/libs/standalone/ocr/proxy_test.go b/libs/standalone/ocr/proxy_test.go new file mode 100644 index 000000000..744b5370e --- /dev/null +++ b/libs/standalone/ocr/proxy_test.go @@ -0,0 +1,65 @@ +package ocr + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/standalone" +) + +func TestProxy(t *testing.T) { + t.Run("the configured peer ID is the one reported and delegated for", func(t *testing.T) { + peerID := mustPeerID(t, 7) + + dep := &proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ + ProxyAddress: "127.0.0.1:50051", + PeerID: peerID, + }} + + factories, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, factories.Close()) }) + + // No keystore was unlocked and no database was opened to learn this: the peer ID is public, + // and the password that would yield it stays with the process hosting the peer. + assert.Equal(t, peerID, factories.PeerID) + assert.Equal(t, peerID.String(), factories.OCR2Endpoint.PeerID()) + assert.Equal(t, peerID.String(), factories.OCR3_1Endpoint.PeerID()) + }) + + t.Run("a peer ID is decoded from its text form", func(t *testing.T) { + // ragetypes.PeerID unmarshals text, so the flags package binds it as a leaf and rejects a + // malformed value when the configuration is decoded rather than when it is first used. + var decoded ragetypes.PeerID + require.NoError(t, decoded.UnmarshalText([]byte(mustPeerID(t, 4).String()))) + assert.Equal(t, mustPeerID(t, 4), decoded) + + require.Error(t, decoded.UnmarshalText([]byte("not-a-peer-id"))) + }) + + t.Run("delegating needs both the proxy address and the peer ID", func(t *testing.T) { + _, err := (&proxyDependency{lggr: logger.Test(t)}).Get(t.Context(), standalone.CommonConfig{}) + require.ErrorContains(t, err, "--ocr.proxy-address is required") + + _, err = (&proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ProxyAddress: "127.0.0.1:50051"}}). + Get(t.Context(), standalone.CommonConfig{}) + require.ErrorContains(t, err, "--ocr.peer-id is required") + }) + + t.Run("an embedded instance needs neither, deriving its identity instead", func(t *testing.T) { + // Nothing is configured here: embedding a delegating dependency yields one with no settings + // at all, the same form a hosted peer yields. + dep := Proxy(logger.Test(t)).ForEmbedding(2) + + factories, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + + assert.Equal(t, mustPeerID(t, 2), factories.PeerID) + }) +} diff --git a/libs/standalone/ports.go b/libs/standalone/ports.go new file mode 100644 index 000000000..ff9e5f795 --- /dev/null +++ b/libs/standalone/ports.go @@ -0,0 +1,46 @@ +package standalone + +import ( + "fmt" + "net" + "strconv" +) + +// OffsetPort returns hostPort with its port raised by delta, keeping the host part as it is: +// OffsetPort(":50051", 2) is ":50053" and OffsetPort("127.0.0.1:1234", 1) is "127.0.0.1:1235". +// +// This is what lets several embedded instances share one configured address: the address a single +// instance would take literally becomes instance i's address by adding i to it, so nothing has to +// be configured per instance. Port 0 is returned unchanged, since every instance asking the OS for +// an ephemeral port already gets a distinct one. +func OffsetPort(hostPort string, delta int) (string, error) { + host, portStr, err := net.SplitHostPort(hostPort) + if err != nil { + return "", fmt.Errorf("failed to parse address %q: %w", hostPort, err) + } + port, err := strconv.Atoi(portStr) + if err != nil { + return "", fmt.Errorf("failed to parse port of address %q: %w", hostPort, err) + } + if port == 0 { + return hostPort, nil + } + return net.JoinHostPort(host, strconv.Itoa(port+delta)), nil +} + +// OffsetPorts applies OffsetPort to every address, returning a new slice. A nil or empty slice +// returns nil, so an unset setting stays unset. +func OffsetPorts(hostPorts []string, delta int) ([]string, error) { + if len(hostPorts) == 0 { + return nil, nil + } + out := make([]string, len(hostPorts)) + for i, hostPort := range hostPorts { + offset, err := OffsetPort(hostPort, delta) + if err != nil { + return nil, err + } + out[i] = offset + } + return out, nil +} diff --git a/libs/standalone/ports_test.go b/libs/standalone/ports_test.go new file mode 100644 index 000000000..5568850b0 --- /dev/null +++ b/libs/standalone/ports_test.go @@ -0,0 +1,58 @@ +package standalone + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOffsetPort(t *testing.T) { + for _, tc := range []struct { + name string + address string + delta int + expected string + }{ + {"an address without a host keeps its shape", ":50051", 2, ":50053"}, + {"instance 0 is the configured address", ":50051", 0, ":50051"}, + {"the host part is left alone", "127.0.0.1:1234", 1, "127.0.0.1:1235"}, + {"an ipv6 host stays bracketed", "[::1]:1234", 1, "[::1]:1235"}, + // Every instance asking for an ephemeral port already gets a distinct one, so offsetting + // would only turn "any port" into a specific one nothing asked for. + {"an ephemeral port is left as it is", "127.0.0.1:0", 3, "127.0.0.1:0"}, + } { + t.Run(tc.name, func(t *testing.T) { + offset, err := OffsetPort(tc.address, tc.delta) + require.NoError(t, err) + assert.Equal(t, tc.expected, offset) + }) + } + + t.Run("an address without a port is an error", func(t *testing.T) { + _, err := OffsetPort("localhost", 1) + require.ErrorContains(t, err, "failed to parse address") + }) + + t.Run("a non-numeric port is an error", func(t *testing.T) { + _, err := OffsetPort("localhost:https", 1) + require.ErrorContains(t, err, "failed to parse port") + }) +} + +func TestOffsetPorts(t *testing.T) { + offset, err := OffsetPorts([]string{"127.0.0.1:100", "127.0.0.1:200"}, 5) + require.NoError(t, err) + assert.Equal(t, []string{"127.0.0.1:105", "127.0.0.1:205"}, offset) + + t.Run("an unset setting stays unset", func(t *testing.T) { + offset, err := OffsetPorts(nil, 1) + require.NoError(t, err) + assert.Nil(t, offset) + }) + + t.Run("one bad address fails the lot", func(t *testing.T) { + _, err := OffsetPorts([]string{"127.0.0.1:100", "nonsense"}, 1) + require.Error(t, err) + }) +} diff --git a/libs/standalone/pyroscope.go b/libs/standalone/pyroscope.go index 787b84b63..d74a4ca7a 100644 --- a/libs/standalone/pyroscope.go +++ b/libs/standalone/pyroscope.go @@ -1,25 +1,20 @@ package standalone import ( - "os" "runtime/debug" "github.com/grafana/pyroscope-go" ) -const ( - envPyroscopeServerAddress = "CL_PYROSCOPE_SERVER_ADDRESS" - envPyroscopeAuthToken = "CL_PYROSCOPE_AUTH_TOKEN" - envPyroscopeEnvironment = "CL_PYROSCOPE_ENVIRONMENT" -) - -// startProfiler starts continuous profiling to a Pyroscope server when -// CL_PYROSCOPE_SERVER_ADDRESS is configured, tagged with appName (typically -// the binary's cobra command name) plus build version/SHA. Returns a nil -// profiler and no error when unconfigured. -func startProfiler(appName string) (*pyroscope.Profiler, error) { - addr := os.Getenv(envPyroscopeServerAddress) - if addr == "" { +// startProfiler starts continuous profiling to a Pyroscope server when one is +// configured, tagged with appName (typically the binary's cobra command name) +// plus build version/SHA. Returns a nil profiler and no error when unconfigured. +// +// One profiler covers the whole process, including every instance of an embed +// run: profiles are per-process by nature, and splitting them per instance would +// only sample the same goroutine scheduler several times. +func startProfiler(appName string, cfg PyroscopeConfig) (*pyroscope.Profiler, error) { + if cfg.ServerAddress == "" { return nil, nil } @@ -34,12 +29,12 @@ func startProfiler(appName string) (*pyroscope.Profiler, error) { return pyroscope.Start(pyroscope.Config{ ApplicationName: appName, - ServerAddress: addr, - AuthToken: os.Getenv(envPyroscopeAuthToken), + ServerAddress: cfg.ServerAddress, + AuthToken: string(cfg.AuthToken), Tags: map[string]string{ "version": ver, "sha": sha, - "environment": os.Getenv(envPyroscopeEnvironment), + "environment": cfg.Environment, }, ProfileTypes: []pyroscope.ProfileType{ pyroscope.ProfileCPU, diff --git a/libs/standalone/telemetry.go b/libs/standalone/telemetry.go index 03dfdfc7b..92eead864 100644 --- a/libs/standalone/telemetry.go +++ b/libs/standalone/telemetry.go @@ -1,20 +1,19 @@ package standalone -// This file copies the pieces of LOOP plugin setup that standalone binaries -// also need — the hclog-compatible logger, the CL_TELEMETRY_*/CL_TRACING_*/ -// CL_CHIP_INGRESS_* env config for beholder, and the prometheus web server -// port — so that they behave the same with or without a plugin host, without -// depending on loop.Server and the full env contract it requires. Tracing -// reuses loop.TracingConfig directly since it's a standalone helper, not part -// of that env contract. +// This file copies the pieces of LOOP plugin setup that standalone binaries also need — the +// hclog-compatible logger and beholder telemetry — so that they behave the same with or without a +// plugin host, without depending on loop.Server and the full env contract it requires. Tracing +// reuses loop.TracingConfig's exporter since it's a standalone helper, not part of that contract. +// +// The settings themselves come from the config structs in config.go rather than from env vars +// directly, so they are flags and config-file keys too; see the note there about why their +// generated env var names still match what a plugin host sets. import ( "context" "fmt" "os" - "strconv" "strings" - "time" prombridge "go.opentelemetry.io/contrib/bridges/prometheus" "go.opentelemetry.io/otel/attribute" @@ -27,25 +26,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/loop" ) -// Env vars mirroring the loop.EnvConfig telemetry subset, set by the plugin -// host (or by the operator, when running standalone). -const ( - envTelemetryEndpoint = "CL_TELEMETRY_ENDPOINT" - envTelemetryInsecureConn = "CL_TELEMETRY_INSECURE_CONNECTION" - envTelemetryCACertFile = "CL_TELEMETRY_CA_CERT_FILE" - envTelemetryAttribute = "CL_TELEMETRY_ATTRIBUTE_" - envTelemetryAuthHeader = "CL_TELEMETRY_AUTH_HEADER" - envTelemetryAuthPubKeyHex = "CL_TELEMETRY_AUTH_PUB_KEY_HEX" - envTelemetryAuthHeadersTTL = "CL_TELEMETRY_AUTH_HEADERS_TTL" - envTelemetryPrometheusBridge = "CL_TELEMETRY_PROMETHEUS_BRIDGE_ENABLED" - envTracingEnabled = "CL_TRACING_ENABLED" - envTracingSamplingRatio = "CL_TRACING_SAMPLING_RATIO" - envTracingTLSCertFile = "CL_TRACING_TLS_CERT_FILE" - envChipIngressEndpoint = "CL_CHIP_INGRESS_ENDPOINT" - envChipIngressInsecureConn = "CL_CHIP_INGRESS_INSECURE_CONNECTION" - envPrometheusPort = "CL_PROMETHEUS_PORT" -) - // Option configures optional Bootstrapper behavior. type Option func(*settings) @@ -54,8 +34,9 @@ type settings struct { } // WithOtelViews sets otel metric views (e.g. histogram bucket boundaries) on -// the beholder client. Views only apply to instruments created after -// NewBootstrapper, since aggregation is fixed when the client is created. +// the beholder client. Views only apply to instruments created after the +// telemetry client is started, which happens once the command runs and its +// configuration has been decoded. func WithOtelViews(otelViews []sdkmetric.View) Option { return func(s *settings) { s.otelViews = append(s.otelViews, otelViews...) } } @@ -75,34 +56,38 @@ func newLogger() (logger.Logger, error) { } // startTelemetry creates, starts, and installs the process-global beholder -// client from the CL_TELEMETRY_* env vars, so instruments created afterwards -// via beholder.GetMeter report over OTLP. When no endpoint is configured it -// returns nil and the global noop client stays: instruments record nothing. -func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder.Client, error) { - endpoint := os.Getenv(envTelemetryEndpoint) - if endpoint == "" { +// client, so instruments created afterwards via beholder.GetMeter report over +// OTLP. When no endpoint is configured it returns nil and the global noop client +// stays: instruments record nothing. +// +// One client serves every instance of an embed run: it is a process-wide export +// pipeline, and which instance recorded a measurement belongs on the instrument's +// attributes rather than in a second exporter. +func startTelemetry(ctx context.Context, o observability, otelViews []sdkmetric.View) (*beholder.Client, error) { + if o.telemetry.Endpoint == "" { return nil, nil } cfg := beholder.DefaultConfig() - cfg.OtelExporterGRPCEndpoint = endpoint - var err error - cfg.InsecureConnection, err = envBool(envTelemetryInsecureConn) + cfg.OtelExporterGRPCEndpoint = o.telemetry.Endpoint + cfg.InsecureConnection = o.telemetry.InsecureConnection + cfg.CACertFile = o.telemetry.CACertFile + + attributes, err := envPairs(envTelemetryAttributePrefix, "telemetry.attributes", o.telemetry.Attributes) if err != nil { return nil, err } - cfg.CACertFile = os.Getenv(envTelemetryCACertFile) - for k, v := range envMap(envTelemetryAttribute) { + for k, v := range attributes { cfg.ResourceAttributes = append(cfg.ResourceAttributes, attribute.String(k, v)) } - cfg.AuthHeaders = envMap(envTelemetryAuthHeader) - cfg.AuthPublicKeyHex = os.Getenv(envTelemetryAuthPubKeyHex) - if s := os.Getenv(envTelemetryAuthHeadersTTL); s != "" { - cfg.AuthHeadersTTL, err = time.ParseDuration(s) - if err != nil { - return nil, fmt.Errorf("failed to parse %s: %w", envTelemetryAuthHeadersTTL, err) - } + + cfg.AuthHeaders, err = envPairs(envTelemetryAuthHeaderPrefix, "telemetry.auth-headers", o.telemetry.AuthHeaders) + if err != nil { + return nil, err } + cfg.AuthPublicKeyHex = o.telemetry.AuthPubKeyHex + cfg.AuthHeadersTTL = o.telemetry.AuthHeadersTTL.Duration() + // Logs already reach their destination via stderr (parsed by the plugin // host when under one); don't stream them a second time. cfg.LogStreamingEnabled = false @@ -110,46 +95,27 @@ func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder. // client is created, so the views cannot be applied any later than this. cfg.MetricViews = otelViews - bridgeEnabled, err := envBool(envTelemetryPrometheusBridge) - if err != nil { - return nil, err - } - if bridgeEnabled { + if o.telemetry.PrometheusBridgeEnabled { // Feeds metrics already registered on the global prometheus registry // (e.g. via promauto, like the health checker's) into the same OTLP // pipeline, so they don't need a separate scrape target. cfg.MetricProducers = append(cfg.MetricProducers, prombridge.NewMetricProducer()) } - cfg.ChipIngressEmitterGRPCEndpoint = os.Getenv(envChipIngressEndpoint) - cfg.ChipIngressEmitterEnabled = cfg.ChipIngressEmitterGRPCEndpoint != "" - if cfg.ChipIngressEmitterEnabled { - cfg.ChipIngressInsecureConnection, err = envBool(envChipIngressInsecureConn) - if err != nil { - return nil, err - } - } + cfg.ChipIngressEmitterGRPCEndpoint = o.chipIngress.Endpoint + cfg.ChipIngressEmitterEnabled = o.chipIngress.Endpoint != "" + cfg.ChipIngressInsecureConnection = o.chipIngress.InsecureConnection - tracingEnabled, err := envBool(envTracingEnabled) - if err != nil { - return nil, err - } - if tracingEnabled { + if o.tracing.Enabled { tracingCfg := loop.TracingConfig{ Enabled: true, - CollectorTarget: endpoint, - SamplingRatio: 1, - TLSCertPath: os.Getenv(envTracingTLSCertFile), + CollectorTarget: o.telemetry.Endpoint, + SamplingRatio: o.tracing.SamplingRatio, + TLSCertPath: o.tracing.TLSCertFile, } if cfg.AuthHeaders != nil { tracingCfg.AuthHeaders = cfg.AuthHeaders } - if s := os.Getenv(envTracingSamplingRatio); s != "" { - tracingCfg.SamplingRatio, err = strconv.ParseFloat(s, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse %s: %w", envTracingSamplingRatio, err) - } - } exporter, err := tracingCfg.NewSpanExporter() if err != nil { @@ -171,38 +137,28 @@ func startTelemetry(ctx context.Context, otelViews []sdkmetric.View) (*beholder. return client, nil } -// envBool parses key as a bool, defaulting to false when unset. -func envBool(key string) (bool, error) { - s := os.Getenv(key) - if s == "" { - return false, nil - } - b, err := strconv.ParseBool(s) +// envPairs merges the key=value pairs of a []string setting with the legacy one-env-var-per-entry +// form a plugin host encodes maps in (loop.EnvConfig.AsCmdEnv): PREFIX_SOME_KEY=value becomes +// SOME_KEY=value. The setting wins on conflict, being the more specific source. Returns nil when +// neither supplies anything. +func envPairs(envPrefix, setting string, pairs []string) (map[string]string, error) { + fromSetting, err := parsePairs(setting, pairs) if err != nil { - return false, fmt.Errorf("failed to parse %s: %w", key, err) + return nil, err } - return b, nil -} -// prometheusPort reports the port for the /metrics and /debug/pprof web -// server from CL_PROMETHEUS_PORT. ok is false when unset, so the server -// stays off unless explicitly configured. Port 0 asks the OS for an -// ephemeral port, e.g. for tests. -func prometheusPort() (port int, ok bool, err error) { - s := os.Getenv(envPrometheusPort) - if s == "" { - return 0, false, nil + merged := envMap(envPrefix) + if merged == nil { + return fromSetting, nil } - port, err = strconv.Atoi(s) - if err != nil { - return 0, false, fmt.Errorf("failed to parse %s: %w", envPrometheusPort, err) + for k, v := range fromSetting { + merged[k] = v } - return port, true, nil + return merged, nil } // envMap collects env vars starting with prefix into a map, with the prefix -// stripped from the keys, mirroring how the plugin host encodes map-valued -// config (loop.EnvConfig.AsCmdEnv). Returns nil when none are set. +// stripped from the keys. Returns nil when none are set. func envMap(prefix string) map[string]string { var m map[string]string for _, env := range os.Environ() { diff --git a/libs/standalone/webserver.go b/libs/standalone/webserver.go new file mode 100644 index 000000000..5869f75df --- /dev/null +++ b/libs/standalone/webserver.go @@ -0,0 +1,103 @@ +package standalone + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/http/pprof" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// webServer serves one instance's /metrics, /debug/pprof and health endpoints. +// +// Each instance gets its own mux and listener rather than sharing net/http's DefaultServeMux (as +// loop.WebServerOpts does): the mux panics on a second registration of the same pattern, so a +// process running several instances could not give each of them a /healthz at all. The pprof +// handlers are therefore registered explicitly too, since importing net/http/pprof only installs +// them on the default mux. +type webServer struct { + lggr logger.Logger + server *http.Server + listener net.Listener +} + +// startWebServer listens on port and serves the endpoints reporting checker's view of one +// instance. Port 0 asks the OS for an ephemeral port, which is logged once bound. +func startWebServer(ctx context.Context, lggr logger.Logger, port int, checker *services.HealthChecker) (*webServer, error) { + mux := http.NewServeMux() + + mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ + EnableOpenMetrics: true, + })) + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + mux.HandleFunc("/healthz", healthHandler(checker.IsHealthy)) + mux.HandleFunc("/readyz", healthHandler(checker.IsReady)) + + // An explicit listener resolves port 0 before Serve, so the chosen port can be logged. + var lc net.ListenConfig + listener, err := lc.Listen(ctx, "tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return nil, fmt.Errorf("failed to listen on port %d: %w", port, err) + } + + w := &webServer{ + lggr: lggr, + listener: listener, + server: &http.Server{ + Handler: mux, + // Reasonable default based on a typical prometheus poll interval of 15s. + ReadTimeout: 5 * time.Second, + }, + } + + go func() { + if err := w.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + w.lggr.Errorw("Metrics and health server stopped", "err", err) + } + }() + + lggr.Infow("Serving metrics and health endpoints", "address", listener.Addr().String()) + return w, nil +} + +func (w *webServer) Close() error { + // Closes the listener too, so the port is free again as soon as this returns. + if err := w.server.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} + +// healthHandler adapts a services.HealthChecker.IsHealthy/IsReady-shaped func into +// an HTTP handler: 200 with each check's status when ok, 503 and the failing +// checks' errors otherwise. +func healthHandler(check func() (bool, map[string]error)) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + ok, errs := check() + if !ok { + w.WriteHeader(http.StatusServiceUnavailable) + } + for name, err := range errs { + if err != nil { + fmt.Fprintf(w, "%s: %s\n", name, err) + } + } + if ok { + fmt.Fprintln(w, "ok") + } + } +} From 9f52ab1eb54f7e41d0f8ece974039d2c8b8ba90c Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Tue, 11 Aug 2026 12:26:30 -0400 Subject: [PATCH 13/30] Init deps in bootstrap --- crecore/main.go | 8 +- crecore/proxy_service.go | 57 +--- crecore/registry_service.go | 34 +- libs/standalone/bootstrap_gen.go | 456 ++++++++++++++++++++++---- libs/standalone/bootstrapper.go | 21 +- libs/standalone/gen/bootstrap.go.tmpl | 25 +- 6 files changed, 449 insertions(+), 152 deletions(-) diff --git a/crecore/main.go b/crecore/main.go index 00f0dcff8..85c42d0a2 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -83,12 +83,12 @@ run "docs" to write the full reference to docs/CONFIG.md.`, return standalone.Run3(bootstrapper, func( ctx context.Context, scfg *standalone.StandaloneConfig, - factories standalone.Dependency[*ocr.Factories], - evmClient standalone.Dependency[evmclient.Client], - lis standalone.Dependency[net.Listener], + factories *ocr.Factories, + evmClient evmclient.Client, + lis net.Listener, ) []services.Service { regSvc := newRegistryService(cfg.CapabilitiesRegistryAddress, cfg.CapabilitiesRegistrySyncInterval.Duration(), - scfg.Logger.Named("capabilities registry"), evmClient, factories) + scfg.Logger.Named("capabilities registry"), evmClient, factories.PeerID) // The registry attaches to the proxy's gRPC server, so core reaches both // over the single address it already configures. proxySvc := newProxyService(scfg.Logger.Named("proxy service"), lis, factories, regSvc.Register) diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index efe5efe92..25b9a67b0 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -8,7 +8,6 @@ import ( "google.golang.org/grpc" "github.com/smartcontractkit/capabilities/crecore/registry" - "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" "github.com/smartcontractkit/chainlink-common/pkg/config" @@ -36,28 +35,26 @@ var defaultConfig = Config{ // proxyService exposes the libocr rage networking factories over gRPC so that // core can delegate its OCR networking (and, in future, DON-to-DON networking) // to this process. The factories come from the ocr bootstrap dependency, which -// either creates a local peer or is backed by another proxy. -// -// NOTE: per the standalone framework, Start blocks until shutdown (the -// Bootstrapper returns the result of Start directly). +// hosts a local peer, is backed by another proxy, or is in-process for an +// embedded instance - this service cannot tell, and neither can core. type proxyService struct { services.Service eng *services.Engine lggr logger.Logger - // listener is where this process serves. Resolved as a dependency rather than opened from a - // configured address, so a process running several instances gives each of them a socket of - // its own without this service knowing that more than one exists. - listener standalone.Dependency[net.Listener] - factories standalone.Dependency[*ocr.Factories] + // listener is where this process serves. It arrives resolved rather than as an address to open, + // so a process running several instances gives each of them a socket of its own without this + // service knowing that more than one exists. Its lifetime is the bootstrapper's, as the + // factories' is: both outlive this service's own start and close. + listener net.Listener + factories *ocr.Factories // registrars attach additional gRPC services to this server before it // Serves. Used so co-located services (e.g. the CapabilitiesRegistry) share // one address instead of each opening a listener. registrars []func(*grpc.Server) - grpcServer *grpc.Server - factoriesClose func() error + grpcServer *grpc.Server } var _ services.Service = (*proxyService)(nil) @@ -65,39 +62,27 @@ var _ services.Service = (*proxyService)(nil) // newProxyService builds the proxy service using the standard // services.Config/Engine pattern, so its lifecycle and health integrate with // the bootstrapper's aggregated health report. -func newProxyService(lggr logger.Logger, listener standalone.Dependency[net.Listener], factories standalone.Dependency[*ocr.Factories], registrars ...func(*grpc.Server)) *proxyService { +func newProxyService(lggr logger.Logger, listener net.Listener, factories *ocr.Factories, registrars ...func(*grpc.Server)) *proxyService { s := &proxyService{lggr: lggr, listener: listener, factories: factories, registrars: registrars} s.Service, s.eng = services.Config{ Name: "P2PProxy", Start: s.start, - Close: s.close, }.NewServiceEngine(lggr) return s } -func (s *proxyService) start(ctx context.Context) error { - factories, err := s.factories.Get(ctx) - if err != nil { - return fmt.Errorf("failed to get libocr factories: %w", err) - } - s.factoriesClose = factories.Close - +func (s *proxyService) start(context.Context) error { metrics, err := newProxyMetrics() if err != nil { return fmt.Errorf("failed to create proxy metrics: %w", err) } - lis, err := s.listener.Get(ctx) - if err != nil { - return fmt.Errorf("failed to get listener: %w", err) - } - // The factories back both surfaces over the same rage connection and // discoverer: OCR endpoints and DON-to-DON peer groups. s.grpcServer = grpc.NewServer() - creproxy.RegisterBinaryNetworkEndpointProxyServer(s.grpcServer, NewServer(factories.OCR2Endpoint, metrics)) - creproxy.RegisterEndpoint2ProxyServer(s.grpcServer, NewEndpoint2Server(factories.OCR3_1Endpoint, metrics)) - creproxy.RegisterPeerGroupProxyServer(s.grpcServer, NewPeerGroupServer(factories.PeerGroup, metrics)) + creproxy.RegisterBinaryNetworkEndpointProxyServer(s.grpcServer, NewServer(s.factories.OCR2Endpoint, metrics)) + creproxy.RegisterEndpoint2ProxyServer(s.grpcServer, NewEndpoint2Server(s.factories.OCR3_1Endpoint, metrics)) + creproxy.RegisterPeerGroupProxyServer(s.grpcServer, NewPeerGroupServer(s.factories.PeerGroup, metrics)) for _, register := range s.registrars { register(s.grpcServer) @@ -111,20 +96,10 @@ func (s *proxyService) start(ctx context.Context) error { s.grpcServer.GracefulStop() }) s.eng.Go(func(context.Context) { - s.lggr.Infow("p2p proxy serving", "address", lis.Addr().String()) - if err := s.grpcServer.Serve(lis); err != nil { + s.lggr.Infow("p2p proxy serving", "address", s.listener.Addr().String()) + if err := s.grpcServer.Serve(s.listener); err != nil { s.eng.Errorw("proxy gRPC server stopped", "err", err) } }) return nil } - -// close tears down the libocr factories (local peer or proxy clients). The gRPC -// server is gracefully stopped by the goroutine started in start once the -// engine cancels its context. -func (s *proxyService) close() error { - if s.factoriesClose != nil { - return s.factoriesClose() - } - return nil -} diff --git a/crecore/registry_service.go b/crecore/registry_service.go index a95efdeb4..d71b2523a 100644 --- a/crecore/registry_service.go +++ b/crecore/registry_service.go @@ -14,8 +14,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/capabilities/crecore/registry" - "github.com/smartcontractkit/capabilities/libs/standalone" - "github.com/smartcontractkit/capabilities/libs/standalone/ocr" regserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/server" @@ -42,9 +40,11 @@ type registryService struct { contractAddress string syncInterval time.Duration - lggr logger.Logger - evm standalone.Dependency[evmclient.Client] - factories standalone.Dependency[*ocr.Factories] + lggr logger.Logger + evm evmclient.Client + // peerID is this node's own, from the same identity the rage networking uses, so the node record + // this process resolves is the node it fronts. + peerID ragetypes.PeerID syncer *registry.Syncer registry *regserver.Registry @@ -56,15 +56,15 @@ func newRegistryService( contractAddress string, syncInterval time.Duration, lggr logger.Logger, - evm standalone.Dependency[evmclient.Client], - factories standalone.Dependency[*ocr.Factories], + evm evmclient.Client, + peerID ragetypes.PeerID, ) *registryService { s := ®istryService{ contractAddress: contractAddress, syncInterval: syncInterval, lggr: lggr, evm: evm, - factories: factories, + peerID: peerID, // The Registry exists from construction so it can be attached to the // proxy's gRPC server before either service starts. Its metadata source is // installed later, in start. @@ -83,23 +83,11 @@ func (s *registryService) start(ctx context.Context) error { return fmt.Errorf("--capabilities-registry-address is required and must be a hex address, got %q", s.contractAddress) } - cl, err := s.evm.Get(ctx) - if err != nil { - return fmt.Errorf("failed to get evm client: %w", err) - } - - // The peer ID comes from the same keystore-backed identity the rage peer uses, - // so the node record this process resolves is the node it fronts. - factories, err := s.factories.Get(ctx) - if err != nil { - return fmt.Errorf("failed to get libocr factories: %w", err) - } - peerID := factories.PeerID - getPeerID := func() (ragetypes.PeerID, error) { return peerID, nil } + getPeerID := func() (ragetypes.PeerID, error) { return s.peerID, nil } syncer, err := registry.NewSyncer( s.lggr, - cl, + s.evm, common.HexToAddress(s.contractAddress), getPeerID, s.syncInterval, @@ -115,7 +103,7 @@ func (s *registryService) start(ctx context.Context) error { } s.lggr.Infow("CapabilitiesRegistry started", - "contract", s.contractAddress, "syncInterval", s.syncInterval, "peerID", peerID.String()) + "contract", s.contractAddress, "syncInterval", s.syncInterval, "peerID", s.peerID.String()) return nil } diff --git a/libs/standalone/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go index 7ad8ff70b..3f350372c 100644 --- a/libs/standalone/bootstrap_gen.go +++ b/libs/standalone/bootstrap_gen.go @@ -4,30 +4,40 @@ package standalone import ( "context" + "fmt" "github.com/smartcontractkit/chainlink-common/pkg/services" ) // Run1 bootstraps the services built from 1 dependency. -// fn receives the context, the StandaloneConfig, and the 1 dependency, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 1 dependency, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependency is resolved before fn is called, and failing to resolve it stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run1[T0 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0) []services.Service, bootDep0 BootstrapDependency[T0], ) error { return bs.run(func(index int, embed bool) instanceServices { dep0 := instanceOf(bs, bootDep0, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + return fn(ctx, cfg, value0), nil } }, []BootstrapCommand{bootDep0}, @@ -39,17 +49,21 @@ func Run1[T0 any]( } // Run2 bootstraps the services built from 2 dependencies. -// fn receives the context, the StandaloneConfig, and the 2 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 2 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run2[T0 any, T1 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], ) error { @@ -59,8 +73,18 @@ func Run2[T0 any, T1 any]( dep1 := instanceOf(bs, bootDep1, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + return fn(ctx, cfg, value0, value1), nil } }, []BootstrapCommand{bootDep0, bootDep1}, @@ -72,17 +96,21 @@ func Run2[T0 any, T1 any]( } // Run3 bootstraps the services built from 3 dependencies. -// fn receives the context, the StandaloneConfig, and the 3 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 3 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run3[T0 any, T1 any, T2 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -95,8 +123,23 @@ func Run3[T0 any, T1 any, T2 any]( dep2 := instanceOf(bs, bootDep2, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2}, @@ -108,17 +151,21 @@ func Run3[T0 any, T1 any, T2 any]( } // Run4 bootstraps the services built from 4 dependencies. -// fn receives the context, the StandaloneConfig, and the 4 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 4 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run4[T0 any, T1 any, T2 any, T3 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -134,8 +181,28 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( dep3 := instanceOf(bs, bootDep3, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2, dep3) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + value3, err := dep3.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 3: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2, value3), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3}, @@ -147,17 +214,21 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( } // Run5 bootstraps the services built from 5 dependencies. -// fn receives the context, the StandaloneConfig, and the 5 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 5 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -176,8 +247,33 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( dep4 := instanceOf(bs, bootDep4, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + value3, err := dep3.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 3: %w", err) + } + + value4, err := dep4.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 4: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2, value3, value4), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4}, @@ -189,17 +285,21 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( } // Run6 bootstraps the services built from 6 dependencies. -// fn receives the context, the StandaloneConfig, and the 6 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 6 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -221,8 +321,38 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( dep5 := instanceOf(bs, bootDep5, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + value3, err := dep3.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 3: %w", err) + } + + value4, err := dep4.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 4: %w", err) + } + + value5, err := dep5.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 5: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2, value3, value4, value5), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5}, @@ -234,17 +364,21 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( } // Run7 bootstraps the services built from 7 dependencies. -// fn receives the context, the StandaloneConfig, and the 7 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 7 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -269,8 +403,43 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( dep6 := instanceOf(bs, bootDep6, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + value3, err := dep3.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 3: %w", err) + } + + value4, err := dep4.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 4: %w", err) + } + + value5, err := dep5.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 5: %w", err) + } + + value6, err := dep6.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 6: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6}, @@ -282,17 +451,21 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( } // Run8 bootstraps the services built from 8 dependencies. -// fn receives the context, the StandaloneConfig, and the 8 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 8 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -320,8 +493,48 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( dep7 := instanceOf(bs, bootDep7, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + value3, err := dep3.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 3: %w", err) + } + + value4, err := dep4.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 4: %w", err) + } + + value5, err := dep5.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 5: %w", err) + } + + value6, err := dep6.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 6: %w", err) + } + + value7, err := dep7.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 7: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7}, @@ -333,17 +546,21 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( } // Run9 bootstraps the services built from 9 dependencies. -// fn receives the context, the StandaloneConfig, and the 9 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 9 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7], dep8 Dependency[T8]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -374,8 +591,53 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any dep8 := instanceOf(bs, bootDep8, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + value3, err := dep3.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 3: %w", err) + } + + value4, err := dep4.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 4: %w", err) + } + + value5, err := dep5.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 5: %w", err) + } + + value6, err := dep6.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 6: %w", err) + } + + value7, err := dep7.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 7: %w", err) + } + + value8, err := dep8.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 8: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7, value8), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8}, @@ -387,17 +649,21 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any } // Run10 bootstraps the services built from 10 dependencies. -// fn receives the context, the StandaloneConfig, and the 10 dependencies, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved 10 dependencies, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The dependencies are resolved before fn is called, and failing to resolve any of them stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, dep0 Dependency[T0], dep1 Dependency[T1], dep2 Dependency[T2], dep3 Dependency[T3], dep4 Dependency[T4], dep5 Dependency[T5], dep6 Dependency[T6], dep7 Dependency[T7], dep8 Dependency[T8], dep9 Dependency[T9]) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8, dep9 T9) []services.Service, bootDep0 BootstrapDependency[T0], bootDep1 BootstrapDependency[T1], bootDep2 BootstrapDependency[T2], @@ -431,8 +697,58 @@ func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 an dep9 := instanceOf(bs, bootDep9, index, embed) - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, dep0, dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8, dep9) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + value0, err := dep0.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 0: %w", err) + } + + value1, err := dep1.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 1: %w", err) + } + + value2, err := dep2.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 2: %w", err) + } + + value3, err := dep3.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 3: %w", err) + } + + value4, err := dep4.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 4: %w", err) + } + + value5, err := dep5.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 5: %w", err) + } + + value6, err := dep6.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 6: %w", err) + } + + value7, err := dep7.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 7: %w", err) + } + + value8, err := dep8.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 8: %w", err) + } + + value9, err := dep9.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency 9: %w", err) + } + + return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7, value8, value9), nil } }, []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9}, diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index f9832a933..d46301ef7 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -147,8 +147,11 @@ func (b *Bootstrapper) registerCloser(v any) { // Logger returns the logger instance. It is safe to call before running the binary func (b *Bootstrapper) Logger() logger.SugaredLogger { return b.config.Logger } -// instanceServices builds the services of one instance, given that instance's StandaloneConfig. -type instanceServices func(ctx context.Context, cfg *StandaloneConfig) []services.Service +// instanceServices resolves one instance's dependencies and builds its services from them, given +// that instance's StandaloneConfig. It fails when a dependency cannot be resolved, which is why the +// services are built here rather than by the engine that supervises them: the engine's constructor +// has nowhere to report that. +type instanceServices func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) // instantiator returns the factory building instance index's services. The generated Run helpers // supply it, since only they know the dependencies' types; embed says whether to replace each @@ -272,9 +275,14 @@ func (b *Bootstrapper) runInstances(ctx context.Context, stop context.CancelFunc func (b *Bootstrapper) startInstance(ctx context.Context, index, count int, factory instanceServices) error { cfg := b.instanceConfig(index, count) + svcs, err := factory(ctx, cfg) + if err != nil { + return err + } + root, _ := services.Config{ Name: instanceName(index, count), - NewSubServices: func(logger.Logger) []services.Service { return factory(ctx, cfg) }, + NewSubServices: func(logger.Logger) []services.Service { return svcs }, }.NewServiceEngine(cfg.Logger) if err := root.Start(ctx); err != nil { @@ -494,10 +502,6 @@ type embedConfig struct { func defaultEmbedConfig() embedConfig { return embedConfig{Instances: 1} } -type Dependency[T any] interface { - Get(ctx context.Context) (T, error) -} - type BootstrapCommand interface { // Config returns the settings to bind, as a pointer to the struct they are decoded into, or nil // when there are none - which is the usual answer from an embedded dependency, having derived or @@ -537,6 +541,9 @@ type BootstrapDependency[T any] interface { BootstrapCommand } +// dependency resolves one instance's copy of a BootstrapDependency, and hands the value's lifetime +// to the bootstrapper: whatever it resolves is closed on shutdown, after the services built from it. +// The generated Run helpers resolve these and pass the values on, so a service never holds one. type dependency[T any] struct { bs *Bootstrapper bd BootstrapDependency[T] diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl index 224b219b4..a30f97ca9 100644 --- a/libs/standalone/gen/bootstrap.go.tmpl +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -2,6 +2,7 @@ package standalone import ( "context" + "fmt" "github.com/smartcontractkit/chainlink-common/pkg/services" ) @@ -9,17 +10,21 @@ import ( {{- range . }} // Run{{.}} bootstraps the services built from {{.}} {{if eq . 1}}dependency{{else}}dependencies{{end}}. -// fn receives the context, the StandaloneConfig, and the {{.}} {{if eq . 1}}dependency{{else}}dependencies{{end}}, and returns the services to run. -// The dependencies are wrapped into a factory that only needs the context, which is handed to bootstrap. +// fn receives the context, the StandaloneConfig, and the resolved {{.}} {{if eq . 1}}dependency{{else}}dependencies{{end}}, and returns the services to run. // The returned services are started together and their health is aggregated by the bootstrapper. // +// The {{if eq . 1}}dependency is{{else}}dependencies are{{end}} resolved before fn is called, and failing to resolve {{if eq . 1}}it{{else}}any of them{{end}} stops the +// binary there. So a service is handed what it needs rather than the means to fetch it: it has one +// less thing to fail at, and a misconfiguration is reported before anything starts rather than +// part-way through starting. +// // fn is called once per instance: a single time for `run`, and once for each of the instances // `embed` starts. For an embedded instance every dependency is first replaced by the form serving -// that instance (BootstrapDependency.ForEmbedding), so a dependency these services resolve is -// already theirs. +// that instance (BootstrapDependency.ForEmbedding), so what fn is handed already belongs to that +// instance. func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( bs *Bootstrapper, - fn func(ctx context.Context, cfg *StandaloneConfig, {{range RangeNum .}}dep{{.}} Dependency[T{{.}}], {{end}}) []services.Service, + fn func(ctx context.Context, cfg *StandaloneConfig, {{range RangeNum .}}dep{{.}} T{{.}}, {{end}}) []services.Service, {{- range RangeNum .}} bootDep{{.}} BootstrapDependency[T{{.}}], {{- end}} @@ -28,8 +33,14 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( {{range RangeNum .}} dep{{.}} := instanceOf(bs, bootDep{{.}}, index, embed) {{end}} - return func(ctx context.Context, cfg *StandaloneConfig) []services.Service { - return fn(ctx, cfg, {{range RangeNum .}}dep{{.}}, {{end}}) + return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { + {{- range RangeNum .}} + value{{.}}, err := dep{{.}}.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve dependency {{.}}: %w", err) + } + {{end}} + return fn(ctx, cfg, {{range RangeNum .}}value{{.}}, {{end}}), nil } }, []BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}, {{end}} }, From 5536125187d2eedbc888a3ca7aaf5b411a09b410 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 12 Aug 2026 02:21:14 -0400 Subject: [PATCH 14/30] Move stuff out so no evm in libs --- crecore/docs/CONFIG.md | 23 +- crecore/main.go | 21 +- crecore/proxy_service.go | 4 - crecore/registry/local_registry.go | 56 ++-- crecore/registry/syncer.go | 218 ++------------ crecore/registry/syncer_test.go | 309 ++++++-------------- crecore/registry_service.go | 53 ++-- libs/go.mod | 43 +-- libs/go.sum | 186 +----------- libs/standalone/bootstrap_gen.go | 151 +++++----- libs/standalone/bootstrapper.go | 98 +------ libs/standalone/db/db_dependency.go | 2 +- libs/standalone/evm/dependency.go | 78 ----- libs/standalone/gen/bootstrap.go.tmpl | 7 +- libs/standalone/listener/dependency.go | 2 +- libs/standalone/listener/dependency_test.go | 2 +- libs/standalone/ocr/embed.go | 2 +- libs/standalone/ocr/host.go | 2 +- libs/standalone/ocr/host_test.go | 2 +- libs/standalone/ocr/proxy.go | 2 +- libs/standalone/ocr/proxy_test.go | 2 +- libs/standalone/ports.go | 46 --- libs/standalone/ports_test.go | 58 ---- 23 files changed, 283 insertions(+), 1084 deletions(-) delete mode 100644 libs/standalone/evm/dependency.go delete mode 100644 libs/standalone/ports.go delete mode 100644 libs/standalone/ports_test.go diff --git a/crecore/docs/CONFIG.md b/crecore/docs/CONFIG.md index 8eeb2f264..3ee2f317b 100644 --- a/crecore/docs/CONFIG.md +++ b/crecore/docs/CONFIG.md @@ -4,7 +4,6 @@ ```toml # ----- Global Configuration ----- -capabilities-registry-address = '0xYourRegistryAddress' capabilities-registry-sync-interval = '12s' [telemetry] endpoint = '' @@ -26,6 +25,8 @@ server-address = '' environment = '' [prometheus] port = -1 +[capabilities-registry] +address = '0xYourRegistryAddress' [evm] http-url = ['https://rpc.example.com'] chain-id = '1' @@ -55,19 +56,12 @@ url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' ## Global ```toml -capabilities-registry-address = '0xYourRegistryAddress' # Example capabilities-registry-sync-interval = '12s' # Default ``` # Global Configuration -### capabilities-registry-address -```toml -capabilities-registry-address = '0xYourRegistryAddress' # Example -``` -capabilities-registry-address on-chain CapabilitiesRegistry (v2) contract address - ### capabilities-registry-sync-interval ```toml capabilities-registry-sync-interval = '12s' # Default @@ -221,6 +215,19 @@ port = -1 # Default ``` port serving /metrics, /debug/pprof, /healthz and /readyz; -1 disables the server, 0 asks the OS for an ephemeral port. Instance i of an embed run listens on this port plus i +## capabilities-registry +```toml +[capabilities-registry] +address = '0xYourRegistryAddress' # Example +``` + + +### address +```toml +address = '0xYourRegistryAddress' # Example +``` +address of the on-chain CapabilitiesRegistry (v2) contract + ## evm ```toml [evm] diff --git a/crecore/main.go b/crecore/main.go index 85c42d0a2..69cd40aec 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -8,15 +8,17 @@ import ( "github.com/spf13/cobra" + "github.com/smartcontractkit/capabilities/crecore/registry" "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/db" - "github.com/smartcontractkit/capabilities/libs/standalone/evm" "github.com/smartcontractkit/capabilities/libs/standalone/listener" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/chainlink-evm/pkg/cre/evm" + evmregistry "github.com/smartcontractkit/chainlink-evm/pkg/cre/registry" + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" "github.com/smartcontractkit/chainlink-common/pkg/services" - evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" ) //go:embed migrations/*.sql @@ -72,9 +74,10 @@ run "docs" to write the full reference to docs/CONFIG.md.`, // that identity and the OCR discoverer table. dbDep := db.Dependency(embeddedMigrations, migrationsTable) ocrDep := ocr.Host(lggr.Named("OCR"), dbDep, ocrDiscovererTable) - // The registry always runs, so the EVM client is always resolved and the - // evm settings are as required in practice as the registry address is. - evmDep := evm.Dependency(lggr.Named("EVM")) + // The registry always runs, so its reader is always resolved. Reading it off a chain is + // chainlink-evm's business: this names the dependency and never sees an EVM type, and the + // client it needs is a dependency of its own rather than one this binary has to hold. + readerDep := evmregistry.Dependency(lggr.Named("CapabilitiesRegistry"), evm.Dependency(lggr.Named("EVM"))) // Where this process serves, as a dependency rather than a setting the proxy service reads: // the address is the one thing two instances in one process cannot agree on, and resolving it // per instance keeps that entirely out of the service. @@ -84,14 +87,14 @@ run "docs" to write the full reference to docs/CONFIG.md.`, ctx context.Context, scfg *standalone.StandaloneConfig, factories *ocr.Factories, - evmClient evmclient.Client, + reader registry.Reader, lis net.Listener, ) []services.Service { - regSvc := newRegistryService(cfg.CapabilitiesRegistryAddress, cfg.CapabilitiesRegistrySyncInterval.Duration(), - scfg.Logger.Named("capabilities registry"), evmClient, factories.PeerID) + regSvc := newRegistryService(cfg.CapabilitiesRegistrySyncInterval.Duration(), + scfg.Logger.Named("capabilities registry"), reader, factories.PeerID) // The registry attaches to the proxy's gRPC server, so core reaches both // over the single address it already configures. proxySvc := newProxyService(scfg.Logger.Named("proxy service"), lis, factories, regSvc.Register) return []services.Service{proxySvc, regSvc} - }, ocrDep, evmDep, listenerDep) + }, ocrDep, readerDep, listenerDep) } diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index 25b9a67b0..522340e17 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -20,10 +20,6 @@ import ( // main.go). The libocr peer configuration lives on the ocr bootstrap dependency, and the address // this process serves on lives on the listener dependency. type Config struct { - // CapabilitiesRegistryAddress is the on-chain CapabilitiesRegistry (v2) contract address. The registry - // always runs, so this is as required in practice as the registry itself. - CapabilitiesRegistryAddress string `usage:"on-chain CapabilitiesRegistry (v2) contract address" validate:"required" example:"'0xYourRegistryAddress'"` - // CapabilitiesRegistrySyncInterval is how often the on-chain registry is re-read. CapabilitiesRegistrySyncInterval config.Duration `usage:"how often the on-chain registry is re-read"` } diff --git a/crecore/registry/local_registry.go b/crecore/registry/local_registry.go index 3a438a034..d6b10a71b 100644 --- a/crecore/registry/local_registry.go +++ b/crecore/registry/local_registry.go @@ -1,9 +1,10 @@ -// Package registry implements the CapabilitiesRegistry that crecore serves over -// plain gRPC (see libs/creregistry/pb). +// Package registry keeps this binary's view of the CapabilitiesRegistry fresh and answerable: who +// the DONs are, which nodes belong to them, which capabilities they host, and which node this is. // -// The on-chain view is read with an EVM client and generated gethwrappers -// directly. There is no relayer, no ContractReader, no codec and no -// ReadIdentifier indirection: three view calls, decoded by the wrapper. +// Where the registry actually lives is not this package's business - a registry.Reader supplies +// whole snapshots from wherever it is written, and the on-chain one lives in chainlink-evm. What is +// here is the Syncer that keeps asking, the snapshot lookups the metadata API needs, and the +// adapter that serves them. package registry import ( @@ -15,43 +16,20 @@ import ( ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + commonregistry "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone/registry" "github.com/smartcontractkit/chainlink-common/pkg/logger" ) -// DonID is a DON's registry ID. -type DonID uint32 - -// DON pairs a DON's identity with the raw capability configuration blobs the -// contract stores for it. -type DON struct { - capabilities.DON - // CapabilityConfigurations maps capability ID to the wire-encoded - // capabilities/pb.CapabilityConfig the contract holds for this DON. - // - // The bytes are deliberately left undecoded. crecore has no reason to - // interpret them: it serves them verbatim over ConfigForCapability and the - // caller unmarshals. That keeps ~150 lines of config-decoding logic (and its - // drift risk) out of this process entirely. - CapabilityConfigurations map[string][]byte -} - -// NodeInfo is a node's on-chain record. -type NodeInfo struct { - NodeOperatorID uint32 - ConfigCount uint32 - WorkflowDONID uint32 - Signer [32]byte - P2pID [32]byte - EncryptionPublicKey [32]byte - CsaKey [32]byte - CapabilityIDs []string -} - -// Capability is a capability's on-chain record. -type Capability struct { - ID string - CapabilityType capabilities.CapabilityType -} +// The registry's own vocabulary comes from the contract a Reader fills in, aliased so this package +// reads as though it owned them. +type ( + DonID = commonregistry.DonID + DON = commonregistry.DON + NodeInfo = commonregistry.NodeInfo + Capability = commonregistry.Capability + Snapshot = commonregistry.Snapshot + Reader = commonregistry.Reader +) // LocalRegistry is an immutable snapshot of the on-chain registry, plus the // lookups the metadata API needs. It is ported from chainlink's diff --git a/crecore/registry/syncer.go b/crecore/registry/syncer.go index 3702c9d05..ac98c06c3 100644 --- a/crecore/registry/syncer.go +++ b/crecore/registry/syncer.go @@ -2,105 +2,58 @@ package registry import ( "context" - "encoding/json" "errors" - "fmt" - "math/big" "sync/atomic" "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - capregv2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" ) -// DefaultSyncInterval matches chainlink's registrysyncer tick so a node fronted -// by crecore sees registry changes on the same cadence it did before. +// DefaultSyncInterval matches chainlink's registrysyncer tick, so a node fronted +// by a standalone process sees registry changes on the same cadence it did before. const DefaultSyncInterval = 12 * time.Second -// pageLimit bounds each view call. The v2 contract's getters are paginated; -// this reads one large page rather than implementing pagination, matching -// chainlink's registrysyncer/v2. If a registry ever exceeds it, the sync logs a -// warning rather than silently truncating. -const pageLimit = 1024 - -// contractCapabilityType is the on-chain capability type enum, in the -// CapabilitiesRegistry contract's ordering. -type contractCapabilityType uint8 - -const ( - contractCapabilityTypeTrigger contractCapabilityType = iota - contractCapabilityTypeAction - contractCapabilityTypeConsensus - contractCapabilityTypeTarget -) - -// capabilityMetadata is the JSON blob the v2 contract stores per capability. -type capabilityMetadata struct { - CapabilityType uint8 `json:"capabilityType"` - ResponseType uint8 `json:"responseType"` -} - -// registryCaller is the subset of the generated v2 wrapper this syncer uses. -// Narrowing it keeps the syncer testable without a chain. -type registryCaller interface { - GetCapabilities(opts *bind.CallOpts, start, limit *big.Int) ([]capregv2.CapabilitiesRegistryCapabilityInfo, error) - GetDONs(opts *bind.CallOpts, start, limit *big.Int) ([]capregv2.CapabilitiesRegistryDONInfo, error) - GetNodes(opts *bind.CallOpts, start, limit *big.Int) ([]capregv2.INodeInfoProviderNodeInfo, error) -} - -// Syncer polls the on-chain CapabilitiesRegistry and publishes immutable -// LocalRegistry snapshots. +// Syncer keeps a current snapshot of the registry by asking a Reader for one on a timer, and +// publishes it to everything that resolves capabilities or their DONs. // -// It polls rather than following events, for the same reason chainlink's syncer -// does: a whole-world snapshot cannot desync from a reorg or a dropped log -// subscription, so there is no missed-event failure mode to reason about. The -// cost is a fixed propagation delay bounded by the tick interval. +// Readers of the snapshot never block on a sync and never see a half-updated view: each sync +// replaces the snapshot pointer wholesale, and a lookup takes whichever snapshot was current when +// it asked. type Syncer struct { services.StateMachine - lggr logger.Logger - caller registryCaller + lggr logger.Logger + reader Reader + interval time.Duration + // getPeerID answers "which node am I", which a snapshot needs to resolve LocalNode and which + // only this process knows. getPeerID func() (ragetypes.PeerID, error) - interval time.Duration - // current holds the most recent successful snapshot, or nil before the - // first successful sync. + // current holds the most recent successful snapshot, or nil before the first successful sync. current atomic.Pointer[LocalRegistry] stopCh services.StopChan done chan struct{} } -// NewSyncer builds a Syncer against an already-dialed EVM client. -// -// backend is typically chainlink-evm's multinode-backed client.Client, which -// satisfies bind.ContractCaller; the reliability behaviour (node health, dead -// node declaration, primary selection) comes from there rather than being -// reimplemented here. -func NewSyncer( - lggr logger.Logger, - backend bind.ContractCaller, - registryAddress common.Address, - getPeerID func() (ragetypes.PeerID, error), - interval time.Duration, -) (*Syncer, error) { - caller, err := capregv2.NewCapabilitiesRegistryCaller(registryAddress, backend) - if err != nil { - return nil, fmt.Errorf("failed to bind CapabilitiesRegistry at %s: %w", registryAddress, err) +// NewSyncer returns a Syncer that keeps up with reader. getPeerID identifies this process to the +// snapshots it publishes. A non-positive interval means DefaultSyncInterval. +func NewSyncer(lggr logger.Logger, reader Reader, getPeerID func() (ragetypes.PeerID, error), interval time.Duration) (*Syncer, error) { + if reader == nil { + return nil, errors.New("a registry reader is required") + } + if getPeerID == nil { + return nil, errors.New("a peer ID is required to resolve this node in the registry") } if interval <= 0 { interval = DefaultSyncInterval } return &Syncer{ lggr: logger.Named(lggr, "RegistrySyncer"), - caller: caller, + reader: reader, getPeerID: getPeerID, interval: interval, stopCh: make(services.StopChan), @@ -125,6 +78,8 @@ func (s *Syncer) Close() error { }) } +// HealthReport is unhealthy until the first snapshot lands: until then nothing this serves can be +// answered, and saying so is more useful than answering every lookup with the same error. func (s *Syncer) HealthReport() map[string]error { err := s.Healthy() if s.current.Load() == nil { @@ -151,8 +106,8 @@ func (s *Syncer) syncLoop() { ticker := time.NewTicker(s.interval) defer ticker.Stop() - // Sync once up front: a ticker first fires at T+interval, and every reader - // of this process blocks until the first snapshot exists. + // Sync once up front: a ticker first fires at T+interval, and every reader of this process + // blocks until the first snapshot exists. if err := s.Sync(ctx); err != nil { s.lggr.Errorw("initial registry sync failed", "err", err) } @@ -169,12 +124,15 @@ func (s *Syncer) syncLoop() { } } -// Sync performs one full read and, on success, publishes a new snapshot. +// Sync performs one read and, on success, publishes a new snapshot. A failed sync leaves the +// previous snapshot in place: a registry that cannot be reached right now is better served stale +// than not at all, and HealthReport is what says so. func (s *Syncer) Sync(ctx context.Context) error { - lr, err := s.importOnchainRegistry(ctx) + snap, err := s.reader.Read(ctx) if err != nil { return err } + lr := NewLocalRegistry(s.lggr, s.getPeerID, snap.DONs, snap.Nodes, snap.Capabilities) s.current.Store(lr) s.lggr.Debugw("registry synced", "capabilities", len(lr.IDsToCapabilities), @@ -182,117 +140,3 @@ func (s *Syncer) Sync(ctx context.Context) error { "nodes", len(lr.IDsToNodes)) return nil } - -func (s *Syncer) importOnchainRegistry(ctx context.Context) (*LocalRegistry, error) { - opts := &bind.CallOpts{Context: ctx} - start, limit := big.NewInt(0), big.NewInt(pageLimit) - - caps, err := s.caller.GetCapabilities(opts, start, limit) - if err != nil { - return nil, fmt.Errorf("getCapabilities: %w", err) - } - s.warnIfTruncated("capabilities", len(caps)) - - idsToCapabilities := make(map[string]Capability, len(caps)) - for _, c := range caps { - if c.IsDeprecated { - continue - } - capType, err := parseCapabilityType(c.Metadata) - if err != nil { - s.lggr.Warnw("failed to parse capability metadata, skipping", - "capabilityID", c.CapabilityId, "err", err) - continue - } - idsToCapabilities[c.CapabilityId] = Capability{ID: c.CapabilityId, CapabilityType: capType} - } - - dons, err := s.caller.GetDONs(opts, start, limit) - if err != nil { - return nil, fmt.Errorf("getDONs: %w", err) - } - s.warnIfTruncated("dons", len(dons)) - - idsToDONs := make(map[DonID]DON, len(dons)) - for _, d := range dons { - cfgs := make(map[string][]byte, len(d.CapabilityConfigurations)) - for _, dc := range d.CapabilityConfigurations { - cfgs[dc.CapabilityId] = dc.Config - } - idsToDONs[DonID(d.Id)] = DON{ - DON: toDON(d), - CapabilityConfigurations: cfgs, - } - } - - nodes, err := s.caller.GetNodes(opts, start, limit) - if err != nil { - return nil, fmt.Errorf("getNodes: %w", err) - } - s.warnIfTruncated("nodes", len(nodes)) - - idsToNodes := make(map[ragetypes.PeerID]NodeInfo, len(nodes)) - for _, n := range nodes { - idsToNodes[n.P2pId] = NodeInfo{ - NodeOperatorID: n.NodeOperatorId, - ConfigCount: n.ConfigCount, - WorkflowDONID: n.WorkflowDONId, - Signer: n.Signer, - P2pID: n.P2pId, - EncryptionPublicKey: n.EncryptionPublicKey, - CsaKey: n.CsaKey, - CapabilityIDs: n.CapabilityIds, - } - } - - return NewLocalRegistry(s.lggr, s.getPeerID, idsToDONs, idsToNodes, idsToCapabilities), nil -} - -// warnIfTruncated makes a hit against pageLimit visible instead of letting a -// full page read as "that is all there is". -func (s *Syncer) warnIfTruncated(what string, n int) { - if n >= pageLimit { - s.lggr.Warnw("registry read hit the page limit; results may be truncated", - "what", what, "count", n, "limit", pageLimit) - } -} - -func toDON(d capregv2.CapabilitiesRegistryDONInfo) capabilities.DON { - members := make([]ragetypes.PeerID, 0, len(d.NodeP2PIds)) - for _, p := range d.NodeP2PIds { - members = append(members, p) - } - return capabilities.DON{ - Name: d.Name, - ID: d.Id, - Families: d.DonFamilies, - ConfigVersion: d.ConfigCount, - Members: members, - F: d.F, - IsPublic: d.IsPublic, - AcceptsWorkflows: d.AcceptsWorkflows, - Config: d.Config, - } -} - -func parseCapabilityType(metadata []byte) (capabilities.CapabilityType, error) { - if len(metadata) == 0 { - return capabilities.CapabilityTypeUnknown, errors.New("metadata is empty") - } - var meta capabilityMetadata - if err := json.Unmarshal(metadata, &meta); err != nil { - return capabilities.CapabilityTypeUnknown, fmt.Errorf("invalid metadata: %w", err) - } - switch contractCapabilityType(meta.CapabilityType) { - case contractCapabilityTypeTrigger: - return capabilities.CapabilityTypeTrigger, nil - case contractCapabilityTypeAction: - return capabilities.CapabilityTypeAction, nil - case contractCapabilityTypeConsensus: - return capabilities.CapabilityTypeConsensus, nil - case contractCapabilityTypeTarget: - return capabilities.CapabilityTypeTarget, nil - default: - return capabilities.CapabilityTypeUnknown, fmt.Errorf("unknown on-chain capability type %d", meta.CapabilityType) - } -} diff --git a/crecore/registry/syncer_test.go b/crecore/registry/syncer_test.go index 9071bc44e..ca27c82d5 100644 --- a/crecore/registry/syncer_test.go +++ b/crecore/registry/syncer_test.go @@ -3,114 +3,94 @@ package registry import ( "context" "errors" - "math/big" "sync" "testing" "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - capregv2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" ) -// fakeCaller stands in for the generated v2 contract caller. -type fakeCaller struct { +// fakeReader stands in for whatever the registry is actually written on. +type fakeReader struct { mu sync.Mutex - caps []capregv2.CapabilitiesRegistryCapabilityInfo - dons []capregv2.CapabilitiesRegistryDONInfo - nodes []capregv2.INodeInfoProviderNodeInfo - - capsErr error - donsErr error - nodesErr error - - calls int - // lastStart/lastLimit record the pagination arguments actually sent. - lastStart, lastLimit *big.Int + snap *Snapshot + err error + reads int + read chan struct{} } -func (f *fakeCaller) GetCapabilities(_ *bind.CallOpts, start, limit *big.Int) ([]capregv2.CapabilitiesRegistryCapabilityInfo, error) { - f.mu.Lock() - defer f.mu.Unlock() - f.calls++ - f.lastStart, f.lastLimit = start, limit - return f.caps, f.capsErr +func newFakeReader() *fakeReader { + return &fakeReader{snap: fakeSnapshot("act@1.0.0"), read: make(chan struct{}, 16)} } -func (f *fakeCaller) GetDONs(*bind.CallOpts, *big.Int, *big.Int) ([]capregv2.CapabilitiesRegistryDONInfo, error) { +func (f *fakeReader) Read(context.Context) (*Snapshot, error) { f.mu.Lock() defer f.mu.Unlock() - return f.dons, f.donsErr + f.reads++ + select { + case f.read <- struct{}{}: + default: + } + if f.err != nil { + return nil, f.err + } + return f.snap, nil } -func (f *fakeCaller) GetNodes(*bind.CallOpts, *big.Int, *big.Int) ([]capregv2.INodeInfoProviderNodeInfo, error) { +func (f *fakeReader) fail(err error) { f.mu.Lock() defer f.mu.Unlock() - return f.nodes, f.nodesErr + f.err = err } -func (f *fakeCaller) callCount() int { +func (f *fakeReader) count() int { f.mu.Lock() defer f.mu.Unlock() - return f.calls + return f.reads +} + +func fakeSnapshot(capabilityID string) *Snapshot { + return &Snapshot{ + // AcceptsWorkflows is what makes this the node's workflow DON, which is what LocalNode + // resolves. + DONs: map[DonID]DON{2: {DON: capabilities.DON{ + ID: 2, Members: []ragetypes.PeerID{peer(1)}, AcceptsWorkflows: true, + }}}, + Nodes: map[ragetypes.PeerID]NodeInfo{peer(1): {WorkflowDONID: 2, P2pID: peer(1)}}, + Capabilities: map[string]Capability{capabilityID: {ID: capabilityID}}, + } } -// newTestSyncer builds a Syncer around a fake caller, bypassing NewSyncer's -// contract binding (which needs a chain backend). -func newTestSyncer(t *testing.T, caller registryCaller) *Syncer { +func newTestSyncer(t *testing.T, reader Reader, interval time.Duration) *Syncer { t.Helper() - s := &Syncer{ - lggr: logger.Test(t), - caller: caller, - getPeerID: func() (ragetypes.PeerID, error) { return peer(1), nil }, - interval: time.Hour, - } - s.stopCh = make(chan struct{}) - s.done = make(chan struct{}) + s, err := NewSyncer(logger.Test(t), reader, func() (ragetypes.PeerID, error) { return peer(1), nil }, interval) + require.NoError(t, err) return s } -func fullCaller() *fakeCaller { - p1, p2 := peer(1), peer(2) - return &fakeCaller{ - caps: []capregv2.CapabilitiesRegistryCapabilityInfo{ - {CapabilityId: "act@1.0.0", Metadata: []byte(`{"capabilityType":1}`)}, - {CapabilityId: "cron@1.0.0", Metadata: []byte(`{"capabilityType":0}`)}, - }, - dons: []capregv2.CapabilitiesRegistryDONInfo{{ - Id: 2, - ConfigCount: 5, - F: 1, - IsPublic: true, - AcceptsWorkflows: true, - NodeP2PIds: [][32]byte{p1, p2}, - DonFamilies: []string{"zone-a"}, - Name: "cap-don", - Config: []byte("don-config"), - CapabilityConfigurations: []capregv2.CapabilitiesRegistryCapabilityConfiguration{ - {CapabilityId: "act@1.0.0", Config: []byte("act-cfg")}, - }, - }}, - nodes: []capregv2.INodeInfoProviderNodeInfo{ - {NodeOperatorId: 11, ConfigCount: 1, WorkflowDONId: 2, P2pId: p1, Signer: [32]byte{0xaa}, CapabilityIds: []string{"act@1.0.0"}}, - {NodeOperatorId: 12, ConfigCount: 1, WorkflowDONId: 2, P2pId: p2}, - }, - } +func TestNewSyncer_RequiresAReaderAndAnIdentity(t *testing.T) { + lggr := logger.Test(t) + getPeerID := func() (ragetypes.PeerID, error) { return peer(1), nil } + + _, err := NewSyncer(lggr, nil, getPeerID, time.Hour) + require.ErrorContains(t, err, "reader is required") + + _, err = NewSyncer(lggr, newFakeReader(), nil, time.Hour) + require.ErrorContains(t, err, "peer ID is required") } func TestSyncer_CurrentBeforeFirstSync(t *testing.T) { - s := newTestSyncer(t, &fakeCaller{}) + s := newTestSyncer(t, newFakeReader(), time.Hour) _, err := s.Current() - require.Error(t, err) - assert.Contains(t, err.Error(), "not synced") + require.ErrorContains(t, err, "not synced") // Health must report the missing snapshot, not "healthy with no data". report := s.HealthReport() @@ -120,190 +100,71 @@ func TestSyncer_CurrentBeforeFirstSync(t *testing.T) { } } -func TestSyncer_SyncBuildsSnapshot(t *testing.T) { - ctx := context.Background() - caller := fullCaller() - s := newTestSyncer(t, caller) +func TestSyncer_SyncPublishesWhatTheReaderReturned(t *testing.T) { + ctx := t.Context() + s := newTestSyncer(t, newFakeReader(), time.Hour) require.NoError(t, s.Sync(ctx)) lr, err := s.Current() require.NoError(t, err) + require.Len(t, lr.IDsToCapabilities, 1) + assert.Contains(t, lr.IDsToCapabilities, "act@1.0.0") - require.Len(t, lr.IDsToCapabilities, 2) - assert.Equal(t, capabilities.CapabilityTypeAction, lr.IDsToCapabilities["act@1.0.0"].CapabilityType) - assert.Equal(t, capabilities.CapabilityTypeTrigger, lr.IDsToCapabilities["cron@1.0.0"].CapabilityType) - - require.Len(t, lr.IDsToDONs, 1) - don := lr.IDsToDONs[2] - assert.Equal(t, "cap-don", don.Name) - assert.Equal(t, uint8(1), don.F) - // ConfigVersion comes from the contract's ConfigCount. - assert.Equal(t, uint32(5), don.ConfigVersion) - assert.Equal(t, []string{"zone-a"}, don.Families) - assert.True(t, don.IsPublic) - assert.True(t, don.AcceptsWorkflows) - assert.Equal(t, []byte("don-config"), don.Config) - require.Len(t, don.Members, 2) - - // Capability config is carried through undecoded. - assert.Equal(t, []byte("act-cfg"), don.CapabilityConfigurations["act@1.0.0"]) - - require.Len(t, lr.IDsToNodes, 2) - assert.Equal(t, uint32(11), lr.IDsToNodes[peer(1)].NodeOperatorID) - assert.Equal(t, [32]byte{0xaa}, lr.IDsToNodes[peer(1)].Signer) - - // A built snapshot is immediately usable end to end. + // The snapshot answers "which node am I" from the identity this process supplied, which is the + // half a reader knows nothing about. node, err := lr.LocalNode(ctx) require.NoError(t, err) assert.Equal(t, uint32(2), node.WorkflowDON.ID) } -func TestSyncer_SyncSendsPagination(t *testing.T) { - ctx := context.Background() - caller := fullCaller() - s := newTestSyncer(t, caller) - - require.NoError(t, s.Sync(ctx)) - assert.Equal(t, int64(0), caller.lastStart.Int64()) - assert.Equal(t, int64(pageLimit), caller.lastLimit.Int64()) -} - -func TestSyncer_SkipsDeprecatedAndUnparseableCapabilities(t *testing.T) { - ctx := context.Background() - - caller := fullCaller() - caller.caps = append(caller.caps, - // Deprecated capabilities are still returned by the contract; treating - // them as live would let workflows resolve a retired capability. - capregv2.CapabilitiesRegistryCapabilityInfo{ - CapabilityId: "old@1.0.0", IsDeprecated: true, Metadata: []byte(`{"capabilityType":1}`), - }, - // One bad metadata blob must not fail the whole sync. - capregv2.CapabilitiesRegistryCapabilityInfo{CapabilityId: "garbage@1.0.0", Metadata: []byte("{")}, - capregv2.CapabilitiesRegistryCapabilityInfo{CapabilityId: "empty@1.0.0", Metadata: nil}, - capregv2.CapabilitiesRegistryCapabilityInfo{CapabilityId: "weird@1.0.0", Metadata: []byte(`{"capabilityType":99}`)}, - ) - - s := newTestSyncer(t, caller) - require.NoError(t, s.Sync(ctx)) - - lr, err := s.Current() - require.NoError(t, err) - assert.Len(t, lr.IDsToCapabilities, 2) - for _, skipped := range []string{"old@1.0.0", "garbage@1.0.0", "empty@1.0.0", "weird@1.0.0"} { - _, ok := lr.IDsToCapabilities[skipped] - assert.False(t, ok, "%s should have been skipped", skipped) - } -} - +// A registry that cannot be reached right now is better served stale than not at all: the previous +// snapshot stays, and health is what says something is wrong. func TestSyncer_SyncErrorsLeavePreviousSnapshot(t *testing.T) { - ctx := context.Background() + ctx := t.Context() + reader := newFakeReader() + s := newTestSyncer(t, reader, time.Hour) - caller := fullCaller() - s := newTestSyncer(t, caller) require.NoError(t, s.Sync(ctx)) + reader.fail(errors.New("rpc down")) - first, err := s.Current() - require.NoError(t, err) - - // A failed read must not replace a good snapshot with a partial one; the node - // keeps operating on the last known-good view until a read succeeds. - caller.mu.Lock() - caller.donsErr = errors.New("rpc down") - caller.mu.Unlock() + require.ErrorContains(t, s.Sync(ctx), "rpc down") - require.Error(t, s.Sync(ctx)) - - second, err := s.Current() + lr, err := s.Current() require.NoError(t, err) - assert.Same(t, first, second) -} - -func TestSyncer_SyncPropagatesEachReadError(t *testing.T) { - ctx := context.Background() - - for _, tc := range []struct { - name string - set func(*fakeCaller) - want string - }{ - {"capabilities", func(c *fakeCaller) { c.capsErr = errors.New("boom") }, "getCapabilities"}, - {"dons", func(c *fakeCaller) { c.donsErr = errors.New("boom") }, "getDONs"}, - {"nodes", func(c *fakeCaller) { c.nodesErr = errors.New("boom") }, "getNodes"}, - } { - t.Run(tc.name, func(t *testing.T) { - caller := fullCaller() - tc.set(caller) - s := newTestSyncer(t, caller) - - err := s.Sync(ctx) - require.Error(t, err) - assert.Contains(t, err.Error(), tc.want) - }) - } + assert.Contains(t, lr.IDsToCapabilities, "act@1.0.0") } func TestSyncer_StartSyncsImmediately(t *testing.T) { - ctx := context.Background() + reader := newFakeReader() + // An interval long enough that anything observed came from the initial sync rather than a tick. + s := newTestSyncer(t, reader, time.Hour) - // A ticker first fires at T+interval, and every reader blocks until the first - // snapshot exists, so Start must not wait a full interval. - caller := fullCaller() - s := newTestSyncer(t, caller) - s.interval = time.Hour - - require.NoError(t, s.Start(ctx)) + require.NoError(t, s.Start(t.Context())) t.Cleanup(func() { require.NoError(t, s.Close()) }) + // The read signal fires as the read starts, so wait for the snapshot rather than for the read: + // what matters is that starting publishes one without waiting for the first tick. require.Eventually(t, func() bool { _, err := s.Current() return err == nil - }, 10*time.Second, 10*time.Millisecond) - - assert.GreaterOrEqual(t, caller.callCount(), 1) - - report := s.HealthReport() - for _, err := range report { - require.NoError(t, err) - } + }, time.Second, 5*time.Millisecond) + assert.Equal(t, 1, reader.count()) } -func TestSyncer_CloseStopsLoop(t *testing.T) { - ctx := context.Background() +func TestSyncer_CloseStopsReading(t *testing.T) { + reader := newFakeReader() + s := newTestSyncer(t, reader, 10*time.Millisecond) - s := newTestSyncer(t, fullCaller()) - require.NoError(t, s.Start(ctx)) + require.NoError(t, s.Start(t.Context())) + select { + case <-reader.read: + case <-time.After(time.Second): + t.Fatal("timed out waiting for the initial sync") + } require.NoError(t, s.Close()) - // Close must be idempotent-safe under the StateMachine contract: a second - // call reports an error rather than panicking on a closed channel. - require.Error(t, s.Close()) -} - -func TestParseCapabilityType(t *testing.T) { - for _, tc := range []struct { - name string - in []byte - want capabilities.CapabilityType - wantErr bool - }{ - {"trigger", []byte(`{"capabilityType":0}`), capabilities.CapabilityTypeTrigger, false}, - {"action", []byte(`{"capabilityType":1}`), capabilities.CapabilityTypeAction, false}, - {"consensus", []byte(`{"capabilityType":2}`), capabilities.CapabilityTypeConsensus, false}, - {"target", []byte(`{"capabilityType":3}`), capabilities.CapabilityTypeTarget, false}, - {"out of range", []byte(`{"capabilityType":9}`), capabilities.CapabilityTypeUnknown, true}, - {"empty", nil, capabilities.CapabilityTypeUnknown, true}, - {"malformed", []byte("nope"), capabilities.CapabilityTypeUnknown, true}, - } { - t.Run(tc.name, func(t *testing.T) { - got, err := parseCapabilityType(tc.in) - if tc.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tc.want, got) - }) - } + after := reader.count() + time.Sleep(50 * time.Millisecond) + assert.Equal(t, after, reader.count(), "the loop kept reading after Close") } diff --git a/crecore/registry_service.go b/crecore/registry_service.go index d71b2523a..ffe29e958 100644 --- a/crecore/registry_service.go +++ b/crecore/registry_service.go @@ -5,27 +5,26 @@ import ( "fmt" "time" - "github.com/ethereum/go-ethereum/common" "google.golang.org/grpc" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/capabilities/crecore/registry" regserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/server" - - evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" ) -// registryService owns the on-chain registry poller and serves the +// registryService keeps the registry snapshot fresh and serves the // CapabilitiesRegistry gRPC service. // +// Where the registry is read from is not its business: it is handed a reader +// (the on-chain one lives in chainlink-evm) and wraps it in the syncer that owns +// the polling, the snapshot and the health of both. +// // There is no enable switch: this binary running is what enables the registry, -// and core does not start without it. So a missing or malformed contract address -// is a startup failure rather than a reason to run degraded. +// and core does not start without it. // // It attaches to the proxy's gRPC server rather than opening a listener of its // own: a node that delegates rage networking to this process already has a @@ -35,13 +34,11 @@ type registryService struct { services.Service eng *services.Engine - // contractAddress is the on-chain CapabilitiesRegistry (v2) address, and - // syncInterval how often it is re-read. Both come from CLI flags; see main.go. - contractAddress string - syncInterval time.Duration + // syncInterval is how often the registry is re-read; see main.go. + syncInterval time.Duration - lggr logger.Logger - evm evmclient.Client + lggr logger.Logger + reader registry.Reader // peerID is this node's own, from the same identity the rage networking uses, so the node record // this process resolves is the node it fronts. peerID ragetypes.PeerID @@ -53,18 +50,16 @@ type registryService struct { var _ services.Service = (*registryService)(nil) func newRegistryService( - contractAddress string, syncInterval time.Duration, lggr logger.Logger, - evm evmclient.Client, + reader registry.Reader, peerID ragetypes.PeerID, ) *registryService { s := ®istryService{ - contractAddress: contractAddress, - syncInterval: syncInterval, - lggr: lggr, - evm: evm, - peerID: peerID, + syncInterval: syncInterval, + lggr: lggr, + reader: reader, + peerID: peerID, // The Registry exists from construction so it can be attached to the // proxy's gRPC server before either service starts. Its metadata source is // installed later, in start. @@ -79,19 +74,9 @@ func newRegistryService( } func (s *registryService) start(ctx context.Context) error { - if !common.IsHexAddress(s.contractAddress) { - return fmt.Errorf("--capabilities-registry-address is required and must be a hex address, got %q", s.contractAddress) - } - getPeerID := func() (ragetypes.PeerID, error) { return s.peerID, nil } - syncer, err := registry.NewSyncer( - s.lggr, - s.evm, - common.HexToAddress(s.contractAddress), - getPeerID, - s.syncInterval, - ) + syncer, err := registry.NewSyncer(s.lggr, s.reader, getPeerID, s.syncInterval) if err != nil { return fmt.Errorf("failed to create registry syncer: %w", err) } @@ -103,7 +88,7 @@ func (s *registryService) start(ctx context.Context) error { } s.lggr.Infow("CapabilitiesRegistry started", - "contract", s.contractAddress, "syncInterval", s.syncInterval, "peerID", s.peerID.String()) + "syncInterval", s.syncInterval, "peerID", s.peerID.String()) return nil } diff --git a/libs/go.mod b/libs/go.mod index ae2944084..01b48370d 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -14,7 +14,6 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-common/keystore v1.3.0 - github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd @@ -51,8 +50,8 @@ require ( github.com/NethermindEth/juno v0.15.11 // indirect github.com/NethermindEth/starknet.go v0.17.1 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 // indirect - github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/XSAM/otelsql v0.42.0 // indirect + github.com/apache/arrow-go/v18 v18.6.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect @@ -80,23 +79,18 @@ require ( github.com/cosmos/gogoproto v1.7.0 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dchest/siphash v1.2.3 // indirect github.com/deckarep/golang-set/v2 v2.9.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect - github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect - github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ethereum/go-ethereum v1.17.3 // indirect github.com/fatih/color v1.19.0 // indirect - github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect @@ -116,9 +110,7 @@ require ( github.com/go-playground/validator/v10 v10.30.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -133,33 +125,25 @@ require ( github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect - github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect - github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db // indirect - github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect - github.com/huin/goupnp v1.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.14.0 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.4 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.6 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -169,10 +153,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect - github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect @@ -181,24 +162,18 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.2.0 // indirect + github.com/onsi/gomega v1.38.2 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect - github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/logging v0.2.4 // indirect - github.com/pion/stun/v2 v2.0.0 // indirect - github.com/pion/transport/v2 v2.2.10 // indirect - github.com/pion/transport/v3 v3.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/rs/cors v1.11.1 // indirect github.com/rs/zerolog v1.33.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect @@ -208,9 +183,6 @@ require ( github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/chain-selectors v1.0.100 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect - github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 // indirect - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect @@ -231,16 +203,10 @@ require ( github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect - github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xssnick/tonutils-go v1.14.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zondax/hid v0.9.2 // indirect @@ -284,9 +250,6 @@ require ( golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect - gopkg.in/guregu/null.v4 v4.0.0 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect pgregory.net/rapid v1.1.0 // indirect @@ -294,5 +257,3 @@ require ( ) replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common - -replace github.com/smartcontractkit/chainlink-evm => ../../chainlink-evm diff --git a/libs/go.sum b/libs/go.sum index c5ceac35d..baaf71c51 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -34,7 +34,6 @@ github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dX github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -55,8 +54,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= -github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow-go/v18 v18.6.0 h1:GX/Jyd3R7mCLiECAwY9FWbbaYblie2WXBSz4Sw8fNpM= github.com/apache/arrow-go/v18 v18.6.0/go.mod h1:gm3MiPpY82fLYK5VKPB3WoJbsiLVDfT7flD5/vHReKw= @@ -98,13 +95,14 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCeN8rge3F/deT52w2WJMmIC5mHNUq+Ut8= github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= @@ -125,8 +123,6 @@ github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= @@ -153,7 +149,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= @@ -239,12 +234,10 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -278,7 +271,6 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -338,7 +330,6 @@ 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -419,59 +410,12 @@ github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsD github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= -github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 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= @@ -499,14 +443,12 @@ github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -516,10 +458,6 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= @@ -531,23 +469,19 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -560,7 +494,6 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= @@ -620,19 +553,15 @@ github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -665,25 +594,17 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= -github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -694,22 +615,18 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= @@ -717,12 +634,6 @@ github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyips github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb h1:HQN56hEteWOBagVDCxV2Fn++frjRI7dfnozEQUq/5ok= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 h1:N1Q6NHuH+T2b6cst43LD2pnykNJSymP/szJJ7rrYY8I= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 h1:71PGTkjdFZ0JrloEC2Fs8eHl1b1gmUuH+bq7q23usKk= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= @@ -768,7 +679,6 @@ github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyi github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= @@ -783,7 +693,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -799,25 +708,15 @@ 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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -829,12 +728,10 @@ github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCn github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= @@ -902,24 +799,16 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= @@ -932,22 +821,10 @@ go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfP golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -958,13 +835,10 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -977,13 +851,11 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= @@ -991,13 +863,6 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1010,8 +875,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1020,15 +883,11 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1053,43 +912,23 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -1098,26 +937,16 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1168,11 +997,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= -gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -1194,7 +1019,6 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= diff --git a/libs/standalone/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go index 3f350372c..46841c4ec 100644 --- a/libs/standalone/bootstrap_gen.go +++ b/libs/standalone/bootstrap_gen.go @@ -6,6 +6,7 @@ import ( "context" "fmt" + contract "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" "github.com/smartcontractkit/chainlink-common/pkg/services" ) @@ -25,7 +26,7 @@ import ( func Run1[T0 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0) []services.Service, - bootDep0 BootstrapDependency[T0], + bootDep0 contract.BootstrapDependency[T0], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -40,11 +41,11 @@ func Run1[T0 any]( return fn(ctx, cfg, value0), nil } }, - []BootstrapCommand{bootDep0}, + []contract.BootstrapCommand{bootDep0}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0)}, ) } @@ -64,8 +65,8 @@ func Run1[T0 any]( func Run2[T0 any, T1 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -87,11 +88,11 @@ func Run2[T0 any, T1 any]( return fn(ctx, cfg, value0, value1), nil } }, - []BootstrapCommand{bootDep0, bootDep1}, + []contract.BootstrapCommand{bootDep0, bootDep1}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0)}, ) } @@ -111,9 +112,9 @@ func Run2[T0 any, T1 any]( func Run3[T0 any, T1 any, T2 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -142,11 +143,11 @@ func Run3[T0 any, T1 any, T2 any]( return fn(ctx, cfg, value0, value1, value2), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0)}, ) } @@ -166,10 +167,10 @@ func Run3[T0 any, T1 any, T2 any]( func Run4[T0 any, T1 any, T2 any, T3 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], - bootDep3 BootstrapDependency[T3], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], + bootDep3 contract.BootstrapDependency[T3], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -205,11 +206,11 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( return fn(ctx, cfg, value0, value1, value2, value3), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0)}, ) } @@ -229,11 +230,11 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], - bootDep3 BootstrapDependency[T3], - bootDep4 BootstrapDependency[T4], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], + bootDep3 contract.BootstrapDependency[T3], + bootDep4 contract.BootstrapDependency[T4], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -276,11 +277,11 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0)}, ) } @@ -300,12 +301,12 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], - bootDep3 BootstrapDependency[T3], - bootDep4 BootstrapDependency[T4], - bootDep5 BootstrapDependency[T5], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], + bootDep3 contract.BootstrapDependency[T3], + bootDep4 contract.BootstrapDependency[T4], + bootDep5 contract.BootstrapDependency[T5], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -355,11 +356,11 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4, value5), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0)}, ) } @@ -379,13 +380,13 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], - bootDep3 BootstrapDependency[T3], - bootDep4 BootstrapDependency[T4], - bootDep5 BootstrapDependency[T5], - bootDep6 BootstrapDependency[T6], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], + bootDep3 contract.BootstrapDependency[T3], + bootDep4 contract.BootstrapDependency[T4], + bootDep5 contract.BootstrapDependency[T5], + bootDep6 contract.BootstrapDependency[T6], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -442,11 +443,11 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0)}, ) } @@ -466,14 +467,14 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], - bootDep3 BootstrapDependency[T3], - bootDep4 BootstrapDependency[T4], - bootDep5 BootstrapDependency[T5], - bootDep6 BootstrapDependency[T6], - bootDep7 BootstrapDependency[T7], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], + bootDep3 contract.BootstrapDependency[T3], + bootDep4 contract.BootstrapDependency[T4], + bootDep5 contract.BootstrapDependency[T5], + bootDep6 contract.BootstrapDependency[T6], + bootDep7 contract.BootstrapDependency[T7], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -537,11 +538,11 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0)}, ) } @@ -561,15 +562,15 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], - bootDep3 BootstrapDependency[T3], - bootDep4 BootstrapDependency[T4], - bootDep5 BootstrapDependency[T5], - bootDep6 BootstrapDependency[T6], - bootDep7 BootstrapDependency[T7], - bootDep8 BootstrapDependency[T8], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], + bootDep3 contract.BootstrapDependency[T3], + bootDep4 contract.BootstrapDependency[T4], + bootDep5 contract.BootstrapDependency[T5], + bootDep6 contract.BootstrapDependency[T6], + bootDep7 contract.BootstrapDependency[T7], + bootDep8 contract.BootstrapDependency[T8], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -640,11 +641,11 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7, value8), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0)}, ) } @@ -664,16 +665,16 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8, dep9 T9) []services.Service, - bootDep0 BootstrapDependency[T0], - bootDep1 BootstrapDependency[T1], - bootDep2 BootstrapDependency[T2], - bootDep3 BootstrapDependency[T3], - bootDep4 BootstrapDependency[T4], - bootDep5 BootstrapDependency[T5], - bootDep6 BootstrapDependency[T6], - bootDep7 BootstrapDependency[T7], - bootDep8 BootstrapDependency[T8], - bootDep9 BootstrapDependency[T9], + bootDep0 contract.BootstrapDependency[T0], + bootDep1 contract.BootstrapDependency[T1], + bootDep2 contract.BootstrapDependency[T2], + bootDep3 contract.BootstrapDependency[T3], + bootDep4 contract.BootstrapDependency[T4], + bootDep5 contract.BootstrapDependency[T5], + bootDep6 contract.BootstrapDependency[T6], + bootDep7 contract.BootstrapDependency[T7], + bootDep8 contract.BootstrapDependency[T8], + bootDep9 contract.BootstrapDependency[T9], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -751,10 +752,10 @@ func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 an return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7, value8, value9), nil } }, - []BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9}, + []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0), bootDep9.ForEmbedding(0)}, + []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0), bootDep9.ForEmbedding(0)}, ) } diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index d46301ef7..3c6d2525b 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -18,6 +18,7 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/chainlink-common/pkg/beholder" + contract "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" "github.com/smartcontractkit/chainlink-common/pkg/config/flags" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" @@ -54,7 +55,7 @@ type Bootstrapper struct { root *cobra.Command settings settings config *StandaloneConfig - commonConfig CommonConfig + commonConfig contract.CommonConfig embedConfig embedConfig observability observability @@ -167,7 +168,7 @@ type instantiator func(index int, embed bool) instanceServices // if a prometheus port is configured, that instance's web server serving /metrics, // /debug/pprof and /healthz + /readyz, then blocks until an interrupt and closes everything in // reverse. -func (b *Bootstrapper) run(instantiate instantiator, configured, embedded []BootstrapCommand) error { +func (b *Bootstrapper) run(instantiate instantiator, configured, embedded []contract.BootstrapCommand) error { defer b.close() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) @@ -354,7 +355,7 @@ func instanceName(index, count int) string { // viper's binding for it at whichever registered last, so the value typed on the command actually // running would be read from the other command's untouched flag - hence the shared ones going to // the root rather than to each subcommand. -func (b *Bootstrapper) setupCommands(runCmd, embedCmd *cobra.Command, configured, embedded []BootstrapCommand) error { +func (b *Bootstrapper) setupCommands(runCmd, embedCmd *cobra.Command, configured, embedded []contract.BootstrapCommand) error { configuredTargets, err := collectTargets(configured) if err != nil { return err @@ -415,12 +416,12 @@ type target struct { // A config must be a pointer, since it is what the configuration is decoded into, or nil when the // dependency has nothing to configure. Anything else is rejected here rather than left to fail // further along: it would also be unusable as a map key, which is how instances are told apart. -func collectTargets(commands []BootstrapCommand) ([]target, error) { +func collectTargets(commands []contract.BootstrapCommand) ([]target, error) { var targets []target seen := map[any]bool{} - var walk func(cmds []BootstrapCommand) error - walk = func(cmds []BootstrapCommand) error { + var walk func(cmds []contract.BootstrapCommand) error + walk = func(cmds []contract.BootstrapCommand) error { for _, cmd := range cmds { if cmd == nil { continue @@ -490,11 +491,6 @@ func underPluginHost() bool { return os.Getenv(h.MagicCookieKey) == h.MagicCookieValue } -// CommonConfig is the process-wide configuration every dependency is handed when it is resolved. -// It says nothing about which instance is resolving it: by then a dependency has already been -// replaced by the form that serves that instance, so there is no decision left for it to make. -type CommonConfig struct{} - // embedConfig is the `embed` subcommand's own configuration. type embedConfig struct { Instances int `usage:"number of instances to run in this process"` @@ -502,51 +498,12 @@ type embedConfig struct { func defaultEmbedConfig() embedConfig { return embedConfig{Instances: 1} } -type BootstrapCommand interface { - // Config returns the settings to bind, as a pointer to the struct they are decoded into, or nil - // when there are none - which is the usual answer from an embedded dependency, having derived or - // replaced everything it would otherwise be told. - Config() any - - Dependencies() []BootstrapCommand - - // Namespace roots this dependency's configuration, so its settings group together and - // same-named settings from different dependencies don't collide - "database" gives - // --database.url, the key database.url and the env var CRE_DATABASE_URL. Return "" to - // keep the settings at the top level. - Namespace() string -} - -type BootstrapDependency[T any] interface { - Get(ctx context.Context, c CommonConfig) (T, error) - - // ForEmbedding returns the dependency instance i of an embedded run resolves instead of this - // one. It is called once per instance, after the configuration has been decoded and before - // anything is resolved, and only for an embedded run - a single instance resolves the - // dependency exactly as configured. - // - // What it returns is already specific to instance i: everything embedding changes - a derived - // identity instead of a stored one, an in-process transport instead of a socket, a schema of - // its own instead of the shared one - is settled here, so Get has no instance to ask about and - // no mode to branch on. A dependency whose embedded form needs none of its settings is free to - // return something else entirely (see ocr.Host), which is also how a setting that only a real - // deployment needs stops being required. - // - // Return the receiver to be shared by every instance, which is what a dependency backed by - // one process-wide resource wants: sharing the dependency shares the single value it resolves - // to. Anything else returns a copy, deep-copying the configuration it adapts so that instances - // never write through to each other's settings. - ForEmbedding(i int) BootstrapDependency[T] - - BootstrapCommand -} - // dependency resolves one instance's copy of a BootstrapDependency, and hands the value's lifetime // to the bootstrapper: whatever it resolves is closed on shutdown, after the services built from it. // The generated Run helpers resolve these and pass the values on, so a service never holds one. type dependency[T any] struct { bs *Bootstrapper - bd BootstrapDependency[T] + bd contract.BootstrapDependency[T] registerOnce sync.Once // guards registering the resolved value with bs, since Get may be called more than once } @@ -565,46 +522,9 @@ func (d *dependency[T]) Get(ctx context.Context) (T, error) { // A single instance keeps the dependency as it was built rather than embedding it at index 0: // `run` is what the configuration describes literally, and a dependency that partitions itself per // instance should not quietly move a single run's state somewhere else. -func instanceOf[T any](bs *Bootstrapper, bd BootstrapDependency[T], index int, embed bool) *dependency[T] { +func instanceOf[T any](bs *Bootstrapper, bd contract.BootstrapDependency[T], index int, embed bool) *dependency[T] { if embed { bd = bd.ForEmbedding(index) } return &dependency[T]{bs: bs, bd: bd} } - -// OnceBootstrapper wraps a BootstrapDependency so that Get is evaluated at most -// onceGet: the first call resolves the dependency and caches its (value, error), -// and every subsequent call returns that same result without re-running Get -// (the ctx and CommonConfig of later calls are ignored). Other commands are all delegated -// -// BootstrapDependency implementations are shared and may have Get called more -// than onceGet — e.g. one dependency resolving another, or the same dependency -// feeding several services — so a New function should wrap its dependency with -// OnceBootstrapper before returning it, making repeated Get calls safe and -// side-effect-free. -func OnceBootstrapper[T any](bd BootstrapDependency[T]) BootstrapDependency[T] { - return &onceBootstrapper[T]{BootstrapDependency: bd} -} - -type onceBootstrapper[T any] struct { - BootstrapDependency[T] - once sync.Once - val T - err error -} - -func (o *onceBootstrapper[T]) Get(ctx context.Context, c CommonConfig) (T, error) { - o.once.Do(func() { o.val, o.err = o.BootstrapDependency.Get(ctx, c) }) - return o.val, o.err -} - -// ForEmbedding gives each instance its own cache, since each resolves its own value - unless the -// wrapped dependency returns itself, meaning it is shared by every instance, in which case this -// wrapper (and the one value it caches) is shared too. -func (o *onceBootstrapper[T]) ForEmbedding(i int) BootstrapDependency[T] { - next := o.BootstrapDependency.ForEmbedding(i) - if next == o.BootstrapDependency { - return o - } - return OnceBootstrapper[T](next) -} diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index 3a74fa70b..6a2e4272b 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -10,7 +10,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) // Dependency returns a standalone.BootstrapDependency that resolves an opened, migrated database. diff --git a/libs/standalone/evm/dependency.go b/libs/standalone/evm/dependency.go deleted file mode 100644 index e2930c761..000000000 --- a/libs/standalone/evm/dependency.go +++ /dev/null @@ -1,78 +0,0 @@ -package evm - -import ( - "context" - "time" - - "github.com/smartcontractkit/chainlink-common/pkg/config" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - - evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" - - "github.com/smartcontractkit/capabilities/libs/standalone" -) - -// Defaults for the settings evmclient.Config exposes, mirroring chainlink's own EVM chain -// defaults for a generic EVM chain, so a standalone binary polling the same RPCs behaves like -// the node does. They live here rather than in the client: this is the instance the flags are -// bound to and decoded into, so an unset setting keeps the value it was given here. -const ( - defaultFinalityDepth = uint32(50) - defaultPollInterval = 10 * time.Second -) - -var defaultConfig = evmclient.Config{ - FinalityTagEnabled: true, - FinalityDepth: defaultFinalityDepth, - PollInterval: *config.MustNewDuration(defaultPollInterval), -} - -// Dependency returns a standalone.BootstrapDependency that resolves a dialed, -// multinode-backed EVM client. The settings and the dialing belong to the client -// itself (evmclient.Config); this binds them as bootstrap configuration under -// evm.*, defaults them, and resolves the client once. -func Dependency(lggr logger.Logger) standalone.BootstrapDependency[evmclient.Client] { - // Wrap in OnceBootstrapper so Get (which dials every configured RPC) runs at - // most once even if several services resolve this dependency. - return standalone.OnceBootstrapper[evmclient.Client](&dependency{lggr: lggr, cfg: defaultConfig}) -} - -type dependency struct { - lggr logger.Logger - - client evmclient.Client - cfg evmclient.Config -} - -var _ standalone.BootstrapDependency[evmclient.Client] = (*dependency)(nil) - -// Namespace groups the EVM settings under evm.* (--evm.http-url, CRE_EVM_HTTP_URL). -func (d *dependency) Namespace() string { return "evm" } - -func (d *dependency) Config() any { - return &d.cfg -} - -func (d *dependency) Dependencies() []standalone.BootstrapCommand { - return []standalone.BootstrapCommand{} -} - -// ForEmbedding returns the receiver, so every embedded instance shares one client: they are -// reading the same chain over the same RPCs, and a client per instance would multiply the request -// rate without telling any of them anything new. -func (d *dependency) ForEmbedding(int) standalone.BootstrapDependency[evmclient.Client] { return d } - -func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (evmclient.Client, error) { - cl, err := evmclient.NewClientFromConfig(ctx, d.lggr, d.cfg) - if err != nil { - return nil, err - } - d.client = cl - return cl, nil -} - -func (d *dependency) Close() { - if d.client != nil { - d.client.Close() - } -} diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl index a30f97ca9..dcb743071 100644 --- a/libs/standalone/gen/bootstrap.go.tmpl +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -4,6 +4,7 @@ import ( "context" "fmt" + contract "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" "github.com/smartcontractkit/chainlink-common/pkg/services" ) @@ -26,7 +27,7 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, {{range RangeNum .}}dep{{.}} T{{.}}, {{end}}) []services.Service, {{- range RangeNum .}} - bootDep{{.}} BootstrapDependency[T{{.}}], + bootDep{{.}} contract.BootstrapDependency[T{{.}}], {{- end}} ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -43,11 +44,11 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( return fn(ctx, cfg, {{range RangeNum .}}value{{.}}, {{end}}), nil } }, - []BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}, {{end}} }, + []contract.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}, {{end}} }, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}.ForEmbedding(0), {{end}} }, + []contract.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}.ForEmbedding(0), {{end}} }, ) } {{- end }} diff --git a/libs/standalone/listener/dependency.go b/libs/standalone/listener/dependency.go index 05f80f92e..bec44c8f7 100644 --- a/libs/standalone/listener/dependency.go +++ b/libs/standalone/listener/dependency.go @@ -17,7 +17,7 @@ import ( "net" "sync" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) // Config is where the server listens. diff --git a/libs/standalone/listener/dependency_test.go b/libs/standalone/listener/dependency_test.go index 1b6de791c..e679d091d 100644 --- a/libs/standalone/listener/dependency_test.go +++ b/libs/standalone/listener/dependency_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) func TestDependency(t *testing.T) { diff --git a/libs/standalone/ocr/embed.go b/libs/standalone/ocr/embed.go index 3d7e8d85b..2970a85e5 100644 --- a/libs/standalone/ocr/embed.go +++ b/libs/standalone/ocr/embed.go @@ -7,7 +7,7 @@ import ( commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) // embedded is one embedded instance's rage networking: an identity derived from the instance index diff --git a/libs/standalone/ocr/host.go b/libs/standalone/ocr/host.go index dc63b7e2a..09b64d3b0 100644 --- a/libs/standalone/ocr/host.go +++ b/libs/standalone/ocr/host.go @@ -17,7 +17,7 @@ import ( commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) // Config is the configuration of a locally hosted libocr peer: the peer's own settings diff --git a/libs/standalone/ocr/host_test.go b/libs/standalone/ocr/host_test.go index 1942053d0..707493ca8 100644 --- a/libs/standalone/ocr/host_test.go +++ b/libs/standalone/ocr/host_test.go @@ -8,7 +8,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) func TestHostForEmbedding(t *testing.T) { diff --git a/libs/standalone/ocr/proxy.go b/libs/standalone/ocr/proxy.go index b4cc2610c..39a01b715 100644 --- a/libs/standalone/ocr/proxy.go +++ b/libs/standalone/ocr/proxy.go @@ -10,7 +10,7 @@ import ( commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) // ProxyConfig is the configuration of a process that delegates its rage networking to a p2p proxy diff --git a/libs/standalone/ocr/proxy_test.go b/libs/standalone/ocr/proxy_test.go index 744b5370e..f2a9786a6 100644 --- a/libs/standalone/ocr/proxy_test.go +++ b/libs/standalone/ocr/proxy_test.go @@ -10,7 +10,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) func TestProxy(t *testing.T) { diff --git a/libs/standalone/ports.go b/libs/standalone/ports.go deleted file mode 100644 index ff9e5f795..000000000 --- a/libs/standalone/ports.go +++ /dev/null @@ -1,46 +0,0 @@ -package standalone - -import ( - "fmt" - "net" - "strconv" -) - -// OffsetPort returns hostPort with its port raised by delta, keeping the host part as it is: -// OffsetPort(":50051", 2) is ":50053" and OffsetPort("127.0.0.1:1234", 1) is "127.0.0.1:1235". -// -// This is what lets several embedded instances share one configured address: the address a single -// instance would take literally becomes instance i's address by adding i to it, so nothing has to -// be configured per instance. Port 0 is returned unchanged, since every instance asking the OS for -// an ephemeral port already gets a distinct one. -func OffsetPort(hostPort string, delta int) (string, error) { - host, portStr, err := net.SplitHostPort(hostPort) - if err != nil { - return "", fmt.Errorf("failed to parse address %q: %w", hostPort, err) - } - port, err := strconv.Atoi(portStr) - if err != nil { - return "", fmt.Errorf("failed to parse port of address %q: %w", hostPort, err) - } - if port == 0 { - return hostPort, nil - } - return net.JoinHostPort(host, strconv.Itoa(port+delta)), nil -} - -// OffsetPorts applies OffsetPort to every address, returning a new slice. A nil or empty slice -// returns nil, so an unset setting stays unset. -func OffsetPorts(hostPorts []string, delta int) ([]string, error) { - if len(hostPorts) == 0 { - return nil, nil - } - out := make([]string, len(hostPorts)) - for i, hostPort := range hostPorts { - offset, err := OffsetPort(hostPort, delta) - if err != nil { - return nil, err - } - out[i] = offset - } - return out, nil -} diff --git a/libs/standalone/ports_test.go b/libs/standalone/ports_test.go deleted file mode 100644 index 5568850b0..000000000 --- a/libs/standalone/ports_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package standalone - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestOffsetPort(t *testing.T) { - for _, tc := range []struct { - name string - address string - delta int - expected string - }{ - {"an address without a host keeps its shape", ":50051", 2, ":50053"}, - {"instance 0 is the configured address", ":50051", 0, ":50051"}, - {"the host part is left alone", "127.0.0.1:1234", 1, "127.0.0.1:1235"}, - {"an ipv6 host stays bracketed", "[::1]:1234", 1, "[::1]:1235"}, - // Every instance asking for an ephemeral port already gets a distinct one, so offsetting - // would only turn "any port" into a specific one nothing asked for. - {"an ephemeral port is left as it is", "127.0.0.1:0", 3, "127.0.0.1:0"}, - } { - t.Run(tc.name, func(t *testing.T) { - offset, err := OffsetPort(tc.address, tc.delta) - require.NoError(t, err) - assert.Equal(t, tc.expected, offset) - }) - } - - t.Run("an address without a port is an error", func(t *testing.T) { - _, err := OffsetPort("localhost", 1) - require.ErrorContains(t, err, "failed to parse address") - }) - - t.Run("a non-numeric port is an error", func(t *testing.T) { - _, err := OffsetPort("localhost:https", 1) - require.ErrorContains(t, err, "failed to parse port") - }) -} - -func TestOffsetPorts(t *testing.T) { - offset, err := OffsetPorts([]string{"127.0.0.1:100", "127.0.0.1:200"}, 5) - require.NoError(t, err) - assert.Equal(t, []string{"127.0.0.1:105", "127.0.0.1:205"}, offset) - - t.Run("an unset setting stays unset", func(t *testing.T) { - offset, err := OffsetPorts(nil, 1) - require.NoError(t, err) - assert.Nil(t, offset) - }) - - t.Run("one bad address fails the lot", func(t *testing.T) { - _, err := OffsetPorts([]string{"127.0.0.1:100", "nonsense"}, 1) - require.Error(t, err) - }) -} From 6fa498448193fef549338eca4c23bc486e57ca43 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 12 Aug 2026 12:19:22 -0400 Subject: [PATCH 15/30] Real reg proxy, need to verify --- .golangci.yml | 11 + crecore/dispatcher_service.go | 123 ++ crecore/go.mod | 10 +- crecore/go.sum | 2 - crecore/main.go | 9 +- crecore/metrics.go | 5 +- crecore/peergroup_server.go | 201 --- crecore/proxy_service.go | 17 +- crecore/registry_service.go | 79 +- libs/go.mod | 15 +- libs/go.sum | 2 - libs/standalone/ocr/dependency.go | 23 +- libs/standalone/ocr/embed.go | 43 +- libs/standalone/ocr/host.go | 27 +- libs/standalone/ocr/inproc.go | 150 +- libs/standalone/ocr/inproc_test.go | 64 - libs/standalone/ocr/peergroup.go | 58 - libs/standalone/ocr/proxy.go | 23 +- libs/x/README.md | 36 + libs/x/don2don/aggregation/default_mode.go | 81 + .../don2don/aggregation/default_mode_test.go | 230 +++ libs/x/don2don/aggregation/signed_report.go | 135 ++ .../don2don/aggregation/signed_report_test.go | 517 +++++++ .../testdata/aggregate_mode_raw_benchmark.txt | 54 + libs/x/don2don/combined_client.go | 120 ++ libs/x/don2don/config.go | 24 + libs/x/don2don/dispatcher.go | 402 +++++ libs/x/don2don/dispatcher_test.go | 339 ++++ libs/x/don2don/doc.go | 30 + libs/x/don2don/executable/client.go | 320 ++++ libs/x/don2don/executable/client_test.go | 635 ++++++++ libs/x/don2don/executable/endtoend_test.go | 561 +++++++ libs/x/don2don/executable/hasher.go | 211 +++ libs/x/don2don/executable/hasher_test.go | 331 ++++ .../executable/request/client_request.go | 618 ++++++++ .../executable/request/server_request.go | 394 +++++ .../request/transmit_schedule_event.go | 8 + libs/x/don2don/executable/server.go | 342 ++++ libs/x/don2don/executable/server_test.go | 1149 ++++++++++++++ libs/x/don2don/messagecache/message_cache.go | 111 ++ .../messagecache/message_cache_test.go | 66 + libs/x/don2don/parallel_executor.go | 122 ++ libs/x/don2don/parallel_executor_test.go | 105 ++ .../local_executable_capability.go | 45 + .../transmission/local_target_capability.go | 57 + .../local_target_capability_test.go | 203 +++ libs/x/don2don/transmission/transmission.go | 144 ++ .../don2don/transmission/transmission_test.go | 103 ++ libs/x/don2don/trigger_publisher.go | 950 ++++++++++++ libs/x/don2don/trigger_publisher_test.go | 1376 +++++++++++++++++ .../don2don/trigger_registration_load_test.go | 758 +++++++++ libs/x/don2don/trigger_subscriber.go | 540 +++++++ libs/x/don2don/trigger_subscriber_test.go | 767 +++++++++ libs/x/don2don/types/messages.pb.go | 528 +++++++ libs/x/don2don/types/messages.proto | 53 + libs/x/don2don/types/mocks/dispatcher.go | 480 ++++++ libs/x/don2don/types/mocks/receiver.go | 72 + libs/x/don2don/types/types.go | 66 + libs/x/don2don/utils.go | 70 + libs/x/don2don/utils_test.go | 120 ++ libs/x/don2don/validation/validation.go | 38 + libs/x/don2don/validation/validation_test.go | 19 + libs/x/internal/synctest/synctest.go | 32 + libs/x/internal/synctest/synctest_race.go | 30 + libs/x/rage/counter.go | 23 + libs/x/rage/counter_test.go | 16 + libs/x/rage/keyring.go | 50 + libs/x/rage/metrics.go | 44 + libs/x/rage/mocks/peer.go | 499 ++++++ libs/x/rage/mocks/peer_wrapper.go | 313 ++++ libs/x/rage/mocks/shared_peer.go | 547 +++++++ libs/x/rage/mocks/signer.go | 135 ++ libs/x/rage/peer.go | 244 +++ libs/x/rage/peer_test.go | 51 + libs/x/rage/shared_peer.go | 402 +++++ libs/x/rage/shared_peer_test.go | 133 ++ libs/x/rage/types.go | 65 + libs/x/rage/utils.go | 32 + libs/x/rage/utils_test.go | 28 + 79 files changed, 16299 insertions(+), 537 deletions(-) create mode 100644 crecore/dispatcher_service.go delete mode 100644 crecore/peergroup_server.go delete mode 100644 libs/standalone/ocr/peergroup.go create mode 100644 libs/x/README.md create mode 100644 libs/x/don2don/aggregation/default_mode.go create mode 100644 libs/x/don2don/aggregation/default_mode_test.go create mode 100644 libs/x/don2don/aggregation/signed_report.go create mode 100644 libs/x/don2don/aggregation/signed_report_test.go create mode 100644 libs/x/don2don/aggregation/testdata/aggregate_mode_raw_benchmark.txt create mode 100644 libs/x/don2don/combined_client.go create mode 100644 libs/x/don2don/config.go create mode 100644 libs/x/don2don/dispatcher.go create mode 100644 libs/x/don2don/dispatcher_test.go create mode 100644 libs/x/don2don/doc.go create mode 100644 libs/x/don2don/executable/client.go create mode 100644 libs/x/don2don/executable/client_test.go create mode 100644 libs/x/don2don/executable/endtoend_test.go create mode 100644 libs/x/don2don/executable/hasher.go create mode 100644 libs/x/don2don/executable/hasher_test.go create mode 100644 libs/x/don2don/executable/request/client_request.go create mode 100644 libs/x/don2don/executable/request/server_request.go create mode 100644 libs/x/don2don/executable/request/transmit_schedule_event.go create mode 100644 libs/x/don2don/executable/server.go create mode 100644 libs/x/don2don/executable/server_test.go create mode 100644 libs/x/don2don/messagecache/message_cache.go create mode 100644 libs/x/don2don/messagecache/message_cache_test.go create mode 100644 libs/x/don2don/parallel_executor.go create mode 100644 libs/x/don2don/parallel_executor_test.go create mode 100644 libs/x/don2don/transmission/local_executable_capability.go create mode 100644 libs/x/don2don/transmission/local_target_capability.go create mode 100644 libs/x/don2don/transmission/local_target_capability_test.go create mode 100644 libs/x/don2don/transmission/transmission.go create mode 100644 libs/x/don2don/transmission/transmission_test.go create mode 100644 libs/x/don2don/trigger_publisher.go create mode 100644 libs/x/don2don/trigger_publisher_test.go create mode 100644 libs/x/don2don/trigger_registration_load_test.go create mode 100644 libs/x/don2don/trigger_subscriber.go create mode 100644 libs/x/don2don/trigger_subscriber_test.go create mode 100644 libs/x/don2don/types/messages.pb.go create mode 100644 libs/x/don2don/types/messages.proto create mode 100644 libs/x/don2don/types/mocks/dispatcher.go create mode 100644 libs/x/don2don/types/mocks/receiver.go create mode 100644 libs/x/don2don/types/types.go create mode 100644 libs/x/don2don/utils.go create mode 100644 libs/x/don2don/utils_test.go create mode 100644 libs/x/don2don/validation/validation.go create mode 100644 libs/x/don2don/validation/validation_test.go create mode 100644 libs/x/internal/synctest/synctest.go create mode 100644 libs/x/internal/synctest/synctest_race.go create mode 100644 libs/x/rage/counter.go create mode 100644 libs/x/rage/counter_test.go create mode 100644 libs/x/rage/keyring.go create mode 100644 libs/x/rage/metrics.go create mode 100644 libs/x/rage/mocks/peer.go create mode 100644 libs/x/rage/mocks/peer_wrapper.go create mode 100644 libs/x/rage/mocks/shared_peer.go create mode 100644 libs/x/rage/mocks/signer.go create mode 100644 libs/x/rage/peer.go create mode 100644 libs/x/rage/peer_test.go create mode 100644 libs/x/rage/shared_peer.go create mode 100644 libs/x/rage/shared_peer_test.go create mode 100644 libs/x/rage/types.go create mode 100644 libs/x/rage/utils.go create mode 100644 libs/x/rage/utils_test.go diff --git a/.golangci.yml b/.golangci.yml index 1531aa24b..87389423c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -174,6 +174,17 @@ linters: - linters: - depguard path: integration_tests + # libs/x holds code moved verbatim from chainlink core while both repos build against it, + # on its way into crecore. It is kept as close to its origin as possible so the move stays + # reviewable as a move, so it is held to core's linting rather than this repo's - including + # the go-ethereum dependency this repo otherwise keeps out. These exclusions go away with + # the package. + - linters: + - depguard + - revive + - staticcheck + - unused + path: libs/x/ paths: - third_party$ - builtin$ diff --git a/crecore/dispatcher_service.go b/crecore/dispatcher_service.go new file mode 100644 index 000000000..5efe4af06 --- /dev/null +++ b/crecore/dispatcher_service.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + "errors" + + "github.com/smartcontractkit/libocr/networking" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonsrv "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + don2don "github.com/smartcontractkit/capabilities/libs/x/don2don" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// DispatcherConfig is the subset of don2don.DispatcherConfig a binary fills in from flags. Field +// names and usage strings mirror don2don.DispatcherConfig so the two stay easy to compare. +type DispatcherConfig struct { + SupportedVersion int `usage:"version stamped on every outgoing DON-to-DON message"` + ReceiverBufferSize int `usage:"how many messages may queue for one receiver before they are dropped"` + + RateLimitGlobalRPS float64 `usage:"inbound DON-to-DON messages allowed per second, across all senders"` + RateLimitGlobalBurst int `usage:"inbound DON-to-DON message burst allowance, across all senders"` + RateLimitPerSenderRPS float64 `usage:"inbound DON-to-DON messages allowed per second, per sender"` + RateLimitPerSenderBurst int `usage:"inbound DON-to-DON message burst allowance, per sender"` +} + +var defaultDispatcherConfig = DispatcherConfig{ + SupportedVersion: 1, + ReceiverBufferSize: 100, + RateLimitGlobalRPS: 100, + RateLimitGlobalBurst: 100, + RateLimitPerSenderRPS: 10, + RateLimitPerSenderBurst: 10, +} + +func (c DispatcherConfig) don2don() don2don.DispatcherConfig { + return don2don.DispatcherConfig{ + SupportedVersion: c.SupportedVersion, + ReceiverBufferSize: c.ReceiverBufferSize, + RateLimit: don2don.DispatcherRateLimit{ + GlobalRPS: c.RateLimitGlobalRPS, + GlobalBurst: c.RateLimitGlobalBurst, + PerSenderRPS: c.RateLimitPerSenderRPS, + PerSenderBurst: c.RateLimitPerSenderBurst, + }, + // This process always sends over the shared peer: there is no "legacy external peer" here, + // unlike core. + SendToSharedPeer: true, + } +} + +// dispatcherService runs don2don.Dispatcher over the same rage connection the OCR proxy serves, +// so this process does the real DON-to-DON work instead of merely fronting core's. +type dispatcherService struct { + commonsrv.Service + eng *commonsrv.Engine + + cfg DispatcherConfig + factories *ocr.RageFactories + registry core.CapabilitiesRegistry + lggr logger.Logger +} + +// newDispatcherService builds the service using the standard services.Config/Engine pattern, so +// its lifecycle and health integrate with the bootstrapper's aggregated health report. +func newDispatcherService(cfg DispatcherConfig, lggr logger.Logger, factories *ocr.RageFactories, registry core.CapabilitiesRegistry) *dispatcherService { + s := &dispatcherService{cfg: cfg, factories: factories, registry: registry, lggr: lggr} + s.Service, s.eng = commonsrv.Config{ + Name: "Dispatcher", + Start: s.start, + }.NewServiceEngine(lggr) + return s +} + +func (s *dispatcherService) start(ctx context.Context) error { + if s.factories.PeerGroup == nil { + return errors.New("no PeerGroup factory: the ocr dependency did not host a real peer") + } + + sharedPeer := rage.NewDon2DonSharedPeer(peerSource{s.factories}, nil, s.lggr) + if err := sharedPeer.Start(ctx); err != nil { + return err + } + + dispatcher, err := don2don.NewDispatcher(s.cfg.don2don(), nil, sharedPeer, signer{s.factories.Keyring}, s.registry, s.lggr) + if err != nil { + _ = sharedPeer.Close() + return err + } + if err := dispatcher.Start(ctx); err != nil { + _ = sharedPeer.Close() + return err + } + + s.eng.Go(func(ctx context.Context) { + <-ctx.Done() + _ = dispatcher.Close() + _ = sharedPeer.Close() + }) + return nil +} + +// peerSource adapts ocr.RageFactories's peer group factory and identity into rage.PeerSource. +type peerSource struct { + factories *ocr.RageFactories +} + +func (p peerSource) PeerGroupFactory() networking.PeerGroupFactory { return p.factories.PeerGroup } +func (p peerSource) PeerID() ragetypes.PeerID { return p.factories.PeerID } + +// signer adapts the peer's own keyring into rage.Signer: the keyring is already unlocked by the +// time this process has a peer, so Initialize has nothing to do. +type signer struct { + keyring ragetypes.PeerKeyring +} + +func (signer) Initialize() error { return nil } + +func (s signer) Sign(data []byte) ([]byte, error) { return s.keyring.Sign(data) } diff --git a/crecore/go.mod b/crecore/go.mod index 3e321a6e3..aadc8b098 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -3,11 +3,9 @@ module github.com/smartcontractkit/capabilities/crecore go 1.26.2 require ( - github.com/ethereum/go-ethereum v1.17.3 github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 - github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 @@ -16,6 +14,7 @@ require ( go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 ) require ( @@ -83,6 +82,7 @@ require ( github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect + github.com/ethereum/go-ethereum v1.17.3 // indirect github.com/fatih/color v1.19.0 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect @@ -205,6 +205,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.100 // indirect github.com/smartcontractkit/chainlink-common/keystore v1.3.0 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 // indirect @@ -282,7 +283,6 @@ require ( google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect - google.golang.org/protobuf v1.36.11 // indirect gopkg.in/guregu/null.v4 v4.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -297,3 +297,7 @@ replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common replace github.com/smartcontractkit/chainlink-evm => ../../chainlink-evm replace github.com/smartcontractkit/capabilities/libs => ../libs + +// Local override: cre/impl/proxy dropped peer-group proxying, only used for the capabilities +// registry move that already has its own proto. +replace github.com/smartcontractkit/chainlink-protos/cre/impl => ../../chainlink-protos/cre/impl diff --git a/crecore/go.sum b/crecore/go.sum index b91fd8ef4..7e0e734e2 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -726,8 +726,6 @@ github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805- github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= -github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d h1:DDoHNG+dMIixwGAQaFUORdreS/fcbw+AotZA1CcrJwY= -github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d/go.mod h1:P38krjjMclE8tCnuBB5/RhC9+ouGaRL6XXRMdw1gVBE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 h1:NJdGFhzT6zMaTod4QkBqVD2sg0I25iw1boOYtTpEwRo= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= diff --git a/crecore/main.go b/crecore/main.go index 69cd40aec..35ecc9bc8 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -86,7 +86,7 @@ run "docs" to write the full reference to docs/CONFIG.md.`, return standalone.Run3(bootstrapper, func( ctx context.Context, scfg *standalone.StandaloneConfig, - factories *ocr.Factories, + factories *ocr.RageFactories, reader registry.Reader, lis net.Listener, ) []services.Service { @@ -94,7 +94,10 @@ run "docs" to write the full reference to docs/CONFIG.md.`, scfg.Logger.Named("capabilities registry"), reader, factories.PeerID) // The registry attaches to the proxy's gRPC server, so core reaches both // over the single address it already configures. - proxySvc := newProxyService(scfg.Logger.Named("proxy service"), lis, factories, regSvc.Register) - return []services.Service{proxySvc, regSvc} + proxySvc := newProxyService(scfg.Logger.Named("proxy service"), lis, &factories.OCRFactories, regSvc.Register) + // The dispatcher runs the real DON-to-DON work over the same rage connection, rather than + // core running it and this process merely fronting it. + dispatcherSvc := newDispatcherService(cfg.Dispatcher, scfg.Logger.Named("dispatcher"), factories, regSvc.CapabilitiesRegistry()) + return []services.Service{proxySvc, regSvc, dispatcherSvc} }, ocrDep, readerDep, listenerDep) } diff --git a/crecore/metrics.go b/crecore/metrics.go index 316657efc..1f3eb3d85 100644 --- a/crecore/metrics.go +++ b/crecore/metrics.go @@ -13,9 +13,8 @@ import ( // Attribute values for the endpoint kind and message direction on proxy metrics. const ( - endpointOCR2 = "ocr2" - endpointOCR3_1 = "ocr3_1" - endpointPeerGroup = "peergroup" + endpointOCR2 = "ocr2" + endpointOCR3_1 = "ocr3_1" directionInbound = "inbound" directionOutbound = "outbound" diff --git a/crecore/peergroup_server.go b/crecore/peergroup_server.go deleted file mode 100644 index 018818add..000000000 --- a/crecore/peergroup_server.go +++ /dev/null @@ -1,201 +0,0 @@ -package main - -import ( - "fmt" - "io" - "sync" - - creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" -) - -// configDigestLen is the byte length of an OCR config digest. -const configDigestLen = 32 - -// PeerGroupServer implements the PeerGroupProxy gRPC service. It is backed by a -// creproxy.PeerGroupFactory (the proxy adapts a real libocr -// networking.PeerGroupFactory to this interface) and exposes DON-to-DON peer -// groups and their streams over the network. -// -// Each Connect stream owns exactly one PeerGroup: the first message must be a -// NewPeerGroupRequest, after which streams are multiplexed over the connection -// by stream_id. Closing the connection closes the group and all its streams. -type PeerGroupServer struct { - creproxy.UnimplementedPeerGroupProxyServer - - pgFactory creproxy.PeerGroupFactory - inboundSizes sizeRecorder - outboundSizes sizeRecorder -} - -// NewPeerGroupServer returns a PeerGroupServer serving groups created by the -// given factory. -func NewPeerGroupServer(pgFactory creproxy.PeerGroupFactory, metrics *proxyMetrics) *PeerGroupServer { - return &PeerGroupServer{ - pgFactory: pgFactory, - inboundSizes: metrics.sizes(endpointPeerGroup, directionInbound), - outboundSizes: metrics.sizes(endpointPeerGroup, directionOutbound), - } -} - -func (s *PeerGroupServer) Connect(stream creproxy.PeerGroupProxy_ConnectServer) error { - req, err := stream.Recv() - if err != nil { - return fmt.Errorf("failed to receive initial NewPeerGroupRequest: %w", err) - } - - newGroupReq, ok := req.Message.(*creproxy.PeerGroupClientRequest_NewPeerGroup) - if !ok { - return fmt.Errorf("first message must be NewPeerGroupRequest, got %T", req.Message) - } - - group, err := s.newPeerGroup(newGroupReq.NewPeerGroup) - if err != nil { - return fmt.Errorf("failed to create peer group: %w", err) - } - - c := &peerGroupConn{ - stream: stream, - group: group, - streams: map[string]creproxy.PeerGroupStream{}, - inboundSizes: s.inboundSizes, - outboundSizes: s.outboundSizes, - } - defer c.close() - - for { - req, err := stream.Recv() - if err != nil { - if err == io.EOF { - return nil - } - return err - } - - switch msg := req.Message.(type) { - case *creproxy.PeerGroupClientRequest_NewPeerGroup: - return fmt.Errorf("NewPeerGroupRequest not allowed after initial setup") - case *creproxy.PeerGroupClientRequest_NewStream: - if err := c.newStream(msg.NewStream); err != nil { - return fmt.Errorf("failed to create stream: %w", err) - } - case *creproxy.PeerGroupClientRequest_StreamSend: - c.sendTo(msg.StreamSend.StreamId, msg.StreamSend.Payload) - case *creproxy.PeerGroupClientRequest_CloseStream: - c.closeStream(msg.CloseStream.StreamId) - } - } -} - -func (s *PeerGroupServer) newPeerGroup(req *creproxy.NewPeerGroupRequest) (creproxy.PeerGroup, error) { - if len(req.ConfigDigest) != configDigestLen { - return nil, fmt.Errorf("invalid config digest length: got %d, expected %d", len(req.ConfigDigest), configDigestLen) - } - var configDigest [configDigestLen]byte - copy(configDigest[:], req.ConfigDigest) - - bootstrappers := make([]creproxy.BootstrapperInfo, len(req.V2Bootstrappers)) - for i, b := range req.V2Bootstrappers { - bootstrappers[i] = creproxy.BootstrapperInfo{ - PeerID: b.PeerId, - Addrs: b.Addrs, - } - } - - return s.pgFactory.NewPeerGroup(configDigest, req.PeerIds, bootstrappers) -} - -// peerGroupConn holds the per-connection state for a single PeerGroup. -type peerGroupConn struct { - stream creproxy.PeerGroupProxy_ConnectServer - group creproxy.PeerGroup - inboundSizes sizeRecorder - outboundSizes sizeRecorder - - // sendMu serializes sends on the gRPC stream, which is written to by one - // goroutine per stream's receive loop. - sendMu sync.Mutex - - mu sync.Mutex - streams map[string]creproxy.PeerGroupStream - wg sync.WaitGroup -} - -func (c *peerGroupConn) send(m *creproxy.PeerGroupServerMessage) error { - c.sendMu.Lock() - defer c.sendMu.Unlock() - return c.stream.Send(m) -} - -func (c *peerGroupConn) newStream(req *creproxy.NewStreamRequest) error { - st, err := c.group.NewStream(req.RemotePeerId, creproxy.StreamArgs{ - StreamName: req.StreamName, - OutgoingBufferSize: int(req.OutgoingBufferSize), - IncomingBufferSize: int(req.IncomingBufferSize), - MaxMessageLength: int(req.MaxMessageLength), - MessagesLimit: rateLimit(req.MessagesLimit), - BytesLimit: rateLimit(req.BytesLimit), - }) - if err != nil { - return err - } - - c.mu.Lock() - c.streams[req.StreamId] = st - c.mu.Unlock() - - streamID := req.StreamId - recv := st.ReceiveMessages() - c.wg.Add(1) - go func() { - defer c.wg.Done() - for payload := range recv { - c.inboundSizes.record(c.stream.Context(), len(payload)) - if err := c.send(&creproxy.PeerGroupServerMessage{ - Message: &creproxy.PeerGroupServerMessage_StreamRecv{ - StreamRecv: &creproxy.StreamRecv{StreamId: streamID, Payload: payload}, - }, - }); err != nil { - return - } - } - }() - return nil -} - -func (c *peerGroupConn) sendTo(streamID string, payload []byte) { - c.mu.Lock() - st := c.streams[streamID] - c.mu.Unlock() - if st != nil { - c.outboundSizes.record(c.stream.Context(), len(payload)) - st.SendMessage(payload) - } -} - -func (c *peerGroupConn) closeStream(streamID string) { - c.mu.Lock() - st := c.streams[streamID] - delete(c.streams, streamID) - c.mu.Unlock() - if st != nil { - _ = st.Close() - } -} - -func (c *peerGroupConn) close() { - c.mu.Lock() - for id, st := range c.streams { - _ = st.Close() - delete(c.streams, id) - } - c.mu.Unlock() - _ = c.group.Close() - c.wg.Wait() -} - -func rateLimit(p *creproxy.TokenBucketParams) creproxy.RateLimit { - if p == nil { - return creproxy.RateLimit{} - } - return creproxy.RateLimit{Rate: p.Rate, Capacity: p.Capacity} -} diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index 522340e17..d45ba217d 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -22,16 +22,19 @@ import ( type Config struct { // CapabilitiesRegistrySyncInterval is how often the on-chain registry is re-read. CapabilitiesRegistrySyncInterval config.Duration `usage:"how often the on-chain registry is re-read"` + + // Dispatcher configures the DON-to-DON dispatcher this process runs over its own rage peer. + Dispatcher DispatcherConfig } var defaultConfig = Config{ CapabilitiesRegistrySyncInterval: *config.MustNewDuration(registry.DefaultSyncInterval), + Dispatcher: defaultDispatcherConfig, } // proxyService exposes the libocr rage networking factories over gRPC so that -// core can delegate its OCR networking (and, in future, DON-to-DON networking) -// to this process. The factories come from the ocr bootstrap dependency, which -// hosts a local peer, is backed by another proxy, or is in-process for an +// core can delegate its OCR networking to this process. The factories come from the ocr bootstrap +// dependency, which hosts a local peer, is backed by another proxy, or is in-process for an // embedded instance - this service cannot tell, and neither can core. type proxyService struct { services.Service @@ -43,7 +46,7 @@ type proxyService struct { // service knowing that more than one exists. Its lifetime is the bootstrapper's, as the // factories' is: both outlive this service's own start and close. listener net.Listener - factories *ocr.Factories + factories *ocr.OCRFactories // registrars attach additional gRPC services to this server before it // Serves. Used so co-located services (e.g. the CapabilitiesRegistry) share @@ -58,7 +61,7 @@ var _ services.Service = (*proxyService)(nil) // newProxyService builds the proxy service using the standard // services.Config/Engine pattern, so its lifecycle and health integrate with // the bootstrapper's aggregated health report. -func newProxyService(lggr logger.Logger, listener net.Listener, factories *ocr.Factories, registrars ...func(*grpc.Server)) *proxyService { +func newProxyService(lggr logger.Logger, listener net.Listener, factories *ocr.OCRFactories, registrars ...func(*grpc.Server)) *proxyService { s := &proxyService{lggr: lggr, listener: listener, factories: factories, registrars: registrars} s.Service, s.eng = services.Config{ Name: "P2PProxy", @@ -73,12 +76,10 @@ func (s *proxyService) start(context.Context) error { return fmt.Errorf("failed to create proxy metrics: %w", err) } - // The factories back both surfaces over the same rage connection and - // discoverer: OCR endpoints and DON-to-DON peer groups. + // The factories back both surfaces over the same rage connection and discoverer. s.grpcServer = grpc.NewServer() creproxy.RegisterBinaryNetworkEndpointProxyServer(s.grpcServer, NewServer(s.factories.OCR2Endpoint, metrics)) creproxy.RegisterEndpoint2ProxyServer(s.grpcServer, NewEndpoint2Server(s.factories.OCR3_1Endpoint, metrics)) - creproxy.RegisterPeerGroupProxyServer(s.grpcServer, NewPeerGroupServer(s.factories.PeerGroup, metrics)) for _, register := range s.registrars { register(s.grpcServer) diff --git a/crecore/registry_service.go b/crecore/registry_service.go index ffe29e958..e70ee2521 100644 --- a/crecore/registry_service.go +++ b/crecore/registry_service.go @@ -6,14 +6,19 @@ import ( "time" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/proto" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/capabilities/crecore/registry" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" regserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/server" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" ) // registryService keeps the registry snapshot fresh and serves the @@ -63,7 +68,11 @@ func newRegistryService( // The Registry exists from construction so it can be attached to the // proxy's gRPC server before either service starts. Its metadata source is // installed later, in start. - registry: regserver.New(lggr), + // + // Capabilities registered here are served on loopback by the same-host LOOP + // process registering them, so insecure credentials are stated explicitly + // rather than defaulted in the client (mirrors creregistry.Select). + registry: regserver.New(lggr, grpc.WithTransportCredentials(insecure.NewCredentials())), } s.Service, s.eng = services.Config{ Name: "CapabilitiesRegistry", @@ -108,3 +117,71 @@ func (s *registryService) close() error { func (s *registryService) Register(srv *grpc.Server) { regserver.Register(srv, s.registry) } + +// CapabilitiesRegistry returns the core.CapabilitiesRegistry don2don.NewDispatcher takes: real +// values (regserver.Registry.Local(), which Add resolves Handles into) plus this service's +// metadata. Both halves are backed by the same registry a LOOP-registered and a dispatcher-reached +// capability go through identically - one gRPC Add, one real entry, callable either way. +func (s *registryService) CapabilitiesRegistry() core.CapabilitiesRegistry { + return registryAdapter{metadata: s.registry, base: s.registry.Local()} +} + +type registryAdapter struct { + metadata *regserver.Registry + base core.CapabilitiesRegistryBase +} + +func (a registryAdapter) Add(ctx context.Context, c capabilities.BaseCapability) error { + return a.base.Add(ctx, c) +} + +func (a registryAdapter) Remove(ctx context.Context, id string) error { + return a.base.Remove(ctx, id) +} + +func (a registryAdapter) Get(ctx context.Context, id string) (capabilities.BaseCapability, error) { + return a.base.Get(ctx, id) +} + +func (a registryAdapter) GetTrigger(ctx context.Context, id string) (capabilities.TriggerCapability, error) { + return a.base.GetTrigger(ctx, id) +} + +func (a registryAdapter) GetExecutable(ctx context.Context, id string) (capabilities.ExecutableCapability, error) { + return a.base.GetExecutable(ctx, id) +} + +func (a registryAdapter) List(ctx context.Context) ([]capabilities.BaseCapability, error) { + return a.base.List(ctx) +} + +func (a registryAdapter) LocalNode(ctx context.Context) (capabilities.Node, error) { + return a.metadata.LocalNode(ctx) +} + +func (a registryAdapter) NodeByPeerID(ctx context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { + return a.metadata.NodeByPeerID(ctx, peerID) +} + +func (a registryAdapter) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { + return a.metadata.DONsForCapability(ctx, capabilityID) +} + +func (a registryAdapter) DONByID(ctx context.Context, donID uint32) (capabilities.DON, error) { + return a.metadata.DONByID(ctx, donID) +} + +// ConfigForCapability decodes the same wire-encoded config the gRPC service's own +// ConfigForCapability RPC parses, since regserver.Registry only stores the raw bytes. +func (a registryAdapter) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { + raw, err := a.metadata.RawConfigForCapability(ctx, capabilityID, donID) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + cfg := &capabilitiespb.CapabilityConfig{} + if err := proto.Unmarshal(raw, cfg); err != nil { + return capabilities.CapabilityConfiguration{}, fmt.Errorf( + "capability %s on DON %d has an unparseable on-chain config: %w", capabilityID, donID, err) + } + return capabilitiespb.CapabilityConfigFromProto(cfg) +} diff --git a/libs/go.mod b/libs/go.mod index 01b48370d..c5d637fd8 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -4,11 +4,14 @@ go 1.26.2 require ( github.com/cenkalti/backoff/v5 v5.0.3 + github.com/ethereum/go-ethereum v1.17.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/grafana/pyroscope-go v1.3.0 github.com/hashicorp/go-plugin v1.8.0 github.com/jmoiron/sqlx v1.4.0 + github.com/mr-tron/base58 v1.3.0 + github.com/pkg/errors v0.9.1 github.com/pressly/goose/v3 v3.27.1 github.com/prometheus/client_golang v1.23.2 github.com/shopspring/decimal v1.4.0 @@ -16,6 +19,8 @@ require ( github.com/smartcontractkit/chainlink-common/keystore v1.3.0 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d + github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 + github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -24,6 +29,7 @@ require ( go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.uber.org/zap v1.28.0 + golang.org/x/crypto v0.52.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 @@ -89,7 +95,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect - github.com/ethereum/go-ethereum v1.17.3 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect @@ -157,7 +162,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect - github.com/mr-tron/base58 v1.3.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect @@ -167,7 +171,6 @@ require ( github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect @@ -186,7 +189,6 @@ require ( github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect - github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect @@ -238,7 +240,6 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect - golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.55.0 // indirect @@ -257,3 +258,7 @@ require ( ) replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common + +// Local override: cre/impl/proxy dropped peer-group proxying, only used for the capabilities +// registry move that already has its own proto. +replace github.com/smartcontractkit/chainlink-protos/cre/impl => ../../chainlink-protos/cre/impl diff --git a/libs/go.sum b/libs/go.sum index baaf71c51..5df9da89f 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -638,8 +638,6 @@ github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-09 github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= -github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d h1:DDoHNG+dMIixwGAQaFUORdreS/fcbw+AotZA1CcrJwY= -github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d/go.mod h1:P38krjjMclE8tCnuBB5/RhC9+ouGaRL6XXRMdw1gVBE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 h1:NJdGFhzT6zMaTod4QkBqVD2sg0I25iw1boOYtTpEwRo= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index 19864e421..9148c0c12 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -27,25 +27,23 @@ import ( "io" "time" + "github.com/smartcontractkit/libocr/networking" ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" - creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" ) -// Factories bundles the libocr rage networking factories the caller serves or drives. Any of the +// OCRFactories bundles the libocr OCR networking factories the caller serves or drives. Any of the // forms above produces it; the caller does not need to know which. Close tears down the underlying // peer (a hosted one) or the proxy client connections (a delegating one), and nothing at all for an // embedded instance, which holds neither. -type Factories struct { +type OCRFactories struct { // OCR2Endpoint creates OCR2 BinaryNetworkEndpoints. OCR2Endpoint ocr2types.BinaryNetworkEndpointFactory // OCR3_1Endpoint creates OCR3.1 BinaryNetworkEndpoint2s. OCR3_1Endpoint ocr2types.BinaryNetworkEndpoint2Factory - // PeerGroup creates DON-to-DON peer groups. - PeerGroup creproxy.PeerGroupFactory // PeerID is the node's rage P2P identity: unlocked from the keystore, configured directly, or // derived from the instance index. Every form resolves it, and consumers other than libocr need @@ -57,13 +55,26 @@ type Factories struct { } // Close releases the underlying peer or proxy clients. -func (f *Factories) Close() error { +func (f *OCRFactories) Close() error { if f == nil || f.closer == nil { return nil } return f.closer.Close() } +// RageFactories is OCRFactories plus what only a real, locally hosted peer can give: a factory for +// DON-to-DON peer groups over that same rage connection, and the keyring to sign with the same key +// it uses. don2don.Dispatcher takes both. +type RageFactories struct { + OCRFactories + + // PeerGroup creates DON-to-DON peer groups. + PeerGroup networking.PeerGroupFactory + // Keyring signs with the same P2P key the peer above uses, for don2don.Dispatcher's + // message-level signatures. + Keyring ragetypes.PeerKeyring +} + // defaultPeerConfig is the peer settings the flags are bound to and decoded into, so an unset // setting keeps the value it is given here. Freshly allocated per call rather than shared, so two // dependencies (or two instances of one) can never decode into the same peer settings. diff --git a/libs/standalone/ocr/embed.go b/libs/standalone/ocr/embed.go index 2970a85e5..4a2a18591 100644 --- a/libs/standalone/ocr/embed.go +++ b/libs/standalone/ocr/embed.go @@ -28,7 +28,7 @@ type embedded struct { index int } -var _ standalone.BootstrapDependency[*Factories] = (*embedded)(nil) +var _ standalone.BootstrapDependency[*RageFactories] = (*embedded)(nil) // Namespace is the same ocr.* the configured forms use, though it names nothing: there is no // configuration here to root under it. @@ -47,11 +47,11 @@ func (d *embedded) Dependencies() []standalone.BootstrapCommand { // ForEmbedding returns the dependency of instance i, so an already-embedded dependency embedded // again is that instance's rather than a nesting of them. -func (d *embedded) ForEmbedding(i int) standalone.BootstrapDependency[*Factories] { +func (d *embedded) ForEmbedding(i int) standalone.BootstrapDependency[*RageFactories] { return &embedded{lggr: d.lggr, index: i} } -func (d *embedded) Get(context.Context, standalone.CommonConfig) (*Factories, error) { +func (d *embedded) Get(context.Context, standalone.CommonConfig) (*RageFactories, error) { keyring, err := DeterministicKeyring(d.index) if err != nil { return nil, err @@ -63,12 +63,35 @@ func (d *embedded) Get(context.Context, standalone.CommonConfig) (*Factories, er "peerID", peerID.String(), ) - // Nothing here is closed: the endpoints and peer groups are closed by whoever created them, and - // the transport itself holds no resource beyond the maps they register in. - return &Factories{ - OCR2Endpoint: ocr2Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, - OCR3_1Endpoint: ocr31Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, - PeerGroup: inprocPeerGroupFactory{net: embedNetwork, peerID: peerID.String()}, - PeerID: peerID, + // Nothing here is closed: the endpoints are closed by whoever created them, and the transport + // itself holds no resource beyond the maps they register in. PeerGroup and Keyring are left + // unset: there is no in-process peer group simulation, so an embedded instance cannot host + // don2don.Dispatcher. + return &RageFactories{ + OCRFactories: OCRFactories{ + OCR2Endpoint: ocr2Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, + OCR3_1Endpoint: ocr31Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, + PeerID: peerID, + }, }, nil } + +// embeddedOCR adapts embedded to standalone.BootstrapDependency[*OCRFactories], for Proxy's +// ForEmbedding: a delegating process only ever needs the OCR half. +type embeddedOCR struct { + *embedded +} + +var _ standalone.BootstrapDependency[*OCRFactories] = embeddedOCR{} + +func (d embeddedOCR) ForEmbedding(i int) standalone.BootstrapDependency[*OCRFactories] { + return embeddedOCR{&embedded{lggr: d.lggr, index: i}} +} + +func (d embeddedOCR) Get(ctx context.Context, cc standalone.CommonConfig) (*OCRFactories, error) { + f, err := d.embedded.Get(ctx, cc) + if err != nil { + return nil, err + } + return &f.OCRFactories, nil +} diff --git a/libs/standalone/ocr/host.go b/libs/standalone/ocr/host.go index 09b64d3b0..37da0c317 100644 --- a/libs/standalone/ocr/host.go +++ b/libs/standalone/ocr/host.go @@ -48,10 +48,10 @@ type Config struct { // table holding p2p announcements. // // An embedded instance resolves neither: see ForEmbedding. -func Host(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*Factories] { +func Host(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*RageFactories] { // Wrap in OnceBootstrapper so Get (which creates the peer) runs at most once even if several // services resolve this dependency. - return standalone.OnceBootstrapper[*Factories](&hostDependency{ + return standalone.OnceBootstrapper[*RageFactories](&hostDependency{ lggr: lggr, db: db, discovererTable: discovererTable, @@ -67,7 +67,7 @@ type hostDependency struct { cfg Config } -var _ standalone.BootstrapDependency[*Factories] = (*hostDependency)(nil) +var _ standalone.BootstrapDependency[*RageFactories] = (*hostDependency)(nil) // Namespace groups the libocr networking settings under ocr.* (--ocr.listen-addresses, // CRE_OCR_LISTEN_ADDRESSES). @@ -82,11 +82,11 @@ func (d *hostDependency) Dependencies() []standalone.BootstrapCommand { // ForEmbedding returns the in-process form, which hosts no peer at all: an embedded instance's peers // are goroutines beside it, so there is nothing for a rage peer to listen on, announce to or // discover. None of this dependency's settings survive into it - see embedded. -func (d *hostDependency) ForEmbedding(i int) standalone.BootstrapDependency[*Factories] { +func (d *hostDependency) ForEmbedding(i int) standalone.BootstrapDependency[*RageFactories] { return &embedded{lggr: d.lggr, index: i} } -func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Factories, error) { +func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (*RageFactories, error) { if len(d.cfg.ListenAddresses) == 0 { return nil, errors.New("--ocr.listen-addresses is required to host a peer") } @@ -123,7 +123,7 @@ func (d *hostDependency) keystoreIdentity(ctx context.Context, cc standalone.Com } // localFactories builds a real libocr peer and exposes its factories. -func (d *hostDependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*Factories, error) { +func (d *hostDependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*RageFactories, error) { discovererDB := ocrcommon.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) d.lggr.Infow("Creating local p2p peer", @@ -151,11 +151,14 @@ func (d *hostDependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyri return nil, fmt.Errorf("failed to create rage peer: %w", err) } - return &Factories{ - OCR2Endpoint: peer.OCR2BinaryNetworkEndpointFactory(), - OCR3_1Endpoint: peer.OCR3_1BinaryNetworkEndpointFactory(), - PeerGroup: newNetworkingPeerGroupFactory(peer.PeerGroupFactory()), - PeerID: peerID, - closer: peer, + return &RageFactories{ + OCRFactories: OCRFactories{ + OCR2Endpoint: peer.OCR2BinaryNetworkEndpointFactory(), + OCR3_1Endpoint: peer.OCR3_1BinaryNetworkEndpointFactory(), + PeerID: peerID, + closer: peer, + }, + PeerGroup: peer.PeerGroupFactory(), + Keyring: keyring, }, nil } diff --git a/libs/standalone/ocr/inproc.go b/libs/standalone/ocr/inproc.go index a4bd4e1a2..03e0c832e 100644 --- a/libs/standalone/ocr/inproc.go +++ b/libs/standalone/ocr/inproc.go @@ -8,8 +8,6 @@ import ( "github.com/smartcontractkit/libocr/commontypes" ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" ) // This file implements the rage networking factories as in-process message passing, for embed @@ -31,12 +29,11 @@ import ( // message in flight the way it could not over a real socket. // network is the in-process transport shared by every instance in the process: a registry of -// mailboxes keyed by config digest and peer ID, plus the streams peer groups open between them. +// mailboxes keyed by config digest and peer ID. type network struct { - mu sync.Mutex - ocr2 map[endpointKey]*ocr2Endpoint - ocr31 map[endpointKey]*binaryNetworkEndpoint2 - streams map[streamKey]*peerGroupStream + mu sync.Mutex + ocr2 map[endpointKey]*ocr2Endpoint + ocr31 map[endpointKey]*binaryNetworkEndpoint2 } // embedNetwork is the process's in-process network. It is package-level for the same reason @@ -47,9 +44,8 @@ var embedNetwork = newNetwork() func newNetwork() *network { return &network{ - ocr2: map[endpointKey]*ocr2Endpoint{}, - ocr31: map[endpointKey]*binaryNetworkEndpoint2{}, - streams: map[streamKey]*peerGroupStream{}, + ocr2: map[endpointKey]*ocr2Endpoint{}, + ocr31: map[endpointKey]*binaryNetworkEndpoint2{}, } } @@ -60,15 +56,6 @@ type endpointKey struct { peerID string } -// streamKey identifies one direction of one peer group stream: the stream owner is where messages -// arrive, and remote is who sends them. -type streamKey struct { - digest [32]byte - name string - owner string - remote string -} - // senderIndex is the oracle ID the receiver knows sender by. The two ends of a link index oracles // by their own copy of the OCR config's peer IDs, so a message carries the sender's position in // the receiver's list, not in the sender's. @@ -358,128 +345,3 @@ var _ ocr2types.RequestHandle = requestHandle{} func (h requestHandle) MakeResponse(payload []byte) ocr2types.OutboundBinaryMessageResponse { return ocr2types.MustMakeOutboundBinaryMessageResponse(h, payload, h.priority) } - -// inprocPeerGroupFactory creates in-process DON-to-DON peer groups for one peer. -type inprocPeerGroupFactory struct { - net *network - peerID string -} - -var _ creproxy.PeerGroupFactory = inprocPeerGroupFactory{} - -func (f inprocPeerGroupFactory) NewPeerGroup(digest [32]byte, peerIDs []string, _ []creproxy.BootstrapperInfo) (creproxy.PeerGroup, error) { - if !slices.Contains(peerIDs, f.peerID) { - return nil, fmt.Errorf("peer %s is not a member of peer group %x", f.peerID, digest) - } - return &inprocPeerGroup{net: f.net, digest: digest, peerID: f.peerID, peerIDs: slices.Clone(peerIDs)}, nil -} - -// inprocPeerGroup hands out streams to other members of the group. Its streams are closed with -// it, per the creproxy.PeerGroup contract. -type inprocPeerGroup struct { - net *network - digest [32]byte - peerID string - peerIDs []string - - mu sync.Mutex - streams []*peerGroupStream - closed bool -} - -var _ creproxy.PeerGroup = (*inprocPeerGroup)(nil) - -func (g *inprocPeerGroup) NewStream(remotePeerID string, args creproxy.StreamArgs) (creproxy.PeerGroupStream, error) { - if !slices.Contains(g.peerIDs, remotePeerID) { - return nil, fmt.Errorf("peer %s is not a member of peer group %x", remotePeerID, g.digest) - } - - s := &peerGroupStream{ - net: g.net, - // Keyed by where messages arrive, so the two ends of the same named stream are each - // other's mirror: what this end sends is looked up under the remote's key. - key: streamKey{digest: g.digest, name: args.StreamName, owner: g.peerID, remote: remotePeerID}, - in: make(chan []byte, bufferSize(args.IncomingBufferSize, defaultBufferSize)), - closed: make(chan struct{}), - } - - g.mu.Lock() - if g.closed { - g.mu.Unlock() - return nil, fmt.Errorf("peer group %x is closed", g.digest) - } - g.streams = append(g.streams, s) - g.mu.Unlock() - - g.net.mu.Lock() - defer g.net.mu.Unlock() - if _, exists := g.net.streams[s.key]; exists { - return nil, fmt.Errorf("stream %q to peer %s already exists in peer group %x", args.StreamName, remotePeerID, g.digest) - } - g.net.streams[s.key] = s - return s, nil -} - -func (g *inprocPeerGroup) Close() error { - g.mu.Lock() - if g.closed { - g.mu.Unlock() - return nil - } - g.closed = true - streams := g.streams - g.streams = nil - g.mu.Unlock() - - var err error - for _, s := range streams { - if cerr := s.Close(); cerr != nil && err == nil { - err = cerr - } - } - return err -} - -// peerGroupStream is one end of a named bidirectional stream between two group members. -type peerGroupStream struct { - net *network - key streamKey - in chan []byte - - closeOnce sync.Once - closed chan struct{} -} - -var _ creproxy.PeerGroupStream = (*peerGroupStream)(nil) - -// SendMessage delivers to the stream the remote end opened back to us, dropping the message if it -// has not opened one yet or its buffer is full - the same outcome as sending on a rage stream to a -// peer that is not connected. -func (s *peerGroupStream) SendMessage(data []byte) { - mirror := streamKey{digest: s.key.digest, name: s.key.name, owner: s.key.remote, remote: s.key.owner} - - s.net.mu.Lock() - peer := s.net.streams[mirror] - s.net.mu.Unlock() - if peer == nil { - return - } - - select { - case <-peer.closed: - case peer.in <- slices.Clone(data): - default: - } -} - -func (s *peerGroupStream) ReceiveMessages() <-chan []byte { return s.in } - -func (s *peerGroupStream) Close() error { - s.closeOnce.Do(func() { - close(s.closed) - s.net.mu.Lock() - delete(s.net.streams, s.key) - s.net.mu.Unlock() - }) - return nil -} diff --git a/libs/standalone/ocr/inproc_test.go b/libs/standalone/ocr/inproc_test.go index 97af5db07..0847d8c97 100644 --- a/libs/standalone/ocr/inproc_test.go +++ b/libs/standalone/ocr/inproc_test.go @@ -9,8 +9,6 @@ import ( "github.com/smartcontractkit/libocr/commontypes" ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" ) // receiveTimeout is how long a test waits for a message that should already be on its way. The @@ -167,68 +165,6 @@ func TestInprocOCR31Endpoints(t *testing.T) { }) } -func TestInprocPeerGroupStreams(t *testing.T) { - net := newNetwork() - peerIDs := testPeerIDs(t, 2) - var digest [32]byte - digest[0] = 42 - - groups := make([]creproxy.PeerGroup, len(peerIDs)) - for i, peerID := range peerIDs { - g, err := inprocPeerGroupFactory{net: net, peerID: peerID}.NewPeerGroup(digest, peerIDs, nil) - require.NoError(t, err) - groups[i] = g - } - - args := creproxy.StreamArgs{StreamName: "stream", IncomingBufferSize: 10, OutgoingBufferSize: 10} - streamFrom0, err := groups[0].NewStream(peerIDs[1], args) - require.NoError(t, err) - streamFrom1, err := groups[1].NewStream(peerIDs[0], args) - require.NoError(t, err) - - t.Run("messages flow both ways between the two ends", func(t *testing.T) { - streamFrom0.SendMessage([]byte("ping")) - assert.Equal(t, []byte("ping"), receive(t, streamFrom1.ReceiveMessages())) - - streamFrom1.SendMessage([]byte("pong")) - assert.Equal(t, []byte("pong"), receive(t, streamFrom0.ReceiveMessages())) - }) - - t.Run("a differently named stream is a different stream", func(t *testing.T) { - other, err := groups[0].NewStream(peerIDs[1], creproxy.StreamArgs{StreamName: "other", IncomingBufferSize: 10}) - require.NoError(t, err) - - // Nothing is listening on "other" at the remote end, so this is dropped rather than - // delivered to the stream that happens to connect the same two peers. - other.SendMessage([]byte("nowhere")) - assertNothingReceived(t, streamFrom1.ReceiveMessages()) - }) - - t.Run("a non-member cannot be streamed to", func(t *testing.T) { - stranger, err := DeterministicPeerID(len(peerIDs) + 1) - require.NoError(t, err) - - _, err = groups[0].NewStream(stranger.String(), args) - require.ErrorContains(t, err, "is not a member of peer group") - }) - - t.Run("closing a group closes its streams and stops delivery", func(t *testing.T) { - require.NoError(t, groups[1].Close()) - - streamFrom0.SendMessage([]byte("after close")) - assertNothingReceived(t, streamFrom1.ReceiveMessages()) - - // Closing again is a no-op, as is closing a stream the group already closed. - require.NoError(t, groups[1].Close()) - require.NoError(t, streamFrom1.Close()) - - _, err := groups[1].NewStream(peerIDs[0], args) - require.ErrorContains(t, err, "is closed") - - require.NoError(t, groups[0].Close()) - }) -} - // receive returns the next value on ch, failing the test if none arrives. func receive[T any](t *testing.T, ch <-chan T) T { t.Helper() diff --git a/libs/standalone/ocr/peergroup.go b/libs/standalone/ocr/peergroup.go deleted file mode 100644 index 4ceea6d07..000000000 --- a/libs/standalone/ocr/peergroup.go +++ /dev/null @@ -1,58 +0,0 @@ -package ocr - -import ( - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/networking" - ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/smartcontractkit/libocr/ragep2p" - - creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" -) - -// newNetworkingPeerGroupFactory adapts a libocr networking.PeerGroupFactory -// (from a local peer) to the creproxy.PeerGroupFactory the proxy server serves. -// The method sets are structurally identical; this only bridges the types so -// create and proxy modes both yield a creproxy.PeerGroupFactory. -func newNetworkingPeerGroupFactory(inner networking.PeerGroupFactory) creproxy.PeerGroupFactory { - return networkingPeerGroupFactory{inner: inner} -} - -type networkingPeerGroupFactory struct { - inner networking.PeerGroupFactory -} - -func (a networkingPeerGroupFactory) NewPeerGroup(configDigest [32]byte, peerIDs []string, bootstrappers []creproxy.BootstrapperInfo) (creproxy.PeerGroup, error) { - locators := make([]commontypes.BootstrapperLocator, len(bootstrappers)) - for i, b := range bootstrappers { - locators[i] = commontypes.BootstrapperLocator{PeerID: b.PeerID, Addrs: b.Addrs} - } - pg, err := a.inner.NewPeerGroup(ocr2types.ConfigDigest(configDigest), peerIDs, locators) - if err != nil { - return nil, err - } - return networkingPeerGroup{inner: pg}, nil -} - -type networkingPeerGroup struct { - inner networking.PeerGroup -} - -func (a networkingPeerGroup) NewStream(remotePeerID string, args creproxy.StreamArgs) (creproxy.PeerGroupStream, error) { - st, err := a.inner.NewStream(remotePeerID, networking.NewStreamArgs1{ - StreamName: args.StreamName, - OutgoingBufferSize: args.OutgoingBufferSize, - IncomingBufferSize: args.IncomingBufferSize, - MaxMessageLength: args.MaxMessageLength, - MessagesLimit: ragep2p.TokenBucketParams{Rate: args.MessagesLimit.Rate, Capacity: args.MessagesLimit.Capacity}, - BytesLimit: ragep2p.TokenBucketParams{Rate: args.BytesLimit.Rate, Capacity: args.BytesLimit.Capacity}, - }) - if err != nil { - return nil, err - } - // networking.Stream's method set matches creproxy.PeerGroupStream. - return st, nil -} - -func (a networkingPeerGroup) Close() error { - return a.inner.Close() -} diff --git a/libs/standalone/ocr/proxy.go b/libs/standalone/ocr/proxy.go index 39a01b715..baf5d732f 100644 --- a/libs/standalone/ocr/proxy.go +++ b/libs/standalone/ocr/proxy.go @@ -44,10 +44,10 @@ type ProxyConfig struct { // It needs no database: everything a delegating process knows about its identity, it is told. // // An embedded instance delegates to nothing: see ForEmbedding. -func Proxy(lggr commonlogger.Logger) standalone.BootstrapDependency[*Factories] { +func Proxy(lggr commonlogger.Logger) standalone.BootstrapDependency[*OCRFactories] { // Wrap in OnceBootstrapper so Get (which dials the proxy) runs at most once even if several // services resolve this dependency. - return standalone.OnceBootstrapper[*Factories](&proxyDependency{lggr: lggr}) + return standalone.OnceBootstrapper[*OCRFactories](&proxyDependency{lggr: lggr}) } type proxyDependency struct { @@ -56,7 +56,7 @@ type proxyDependency struct { cfg ProxyConfig } -var _ standalone.BootstrapDependency[*Factories] = (*proxyDependency)(nil) +var _ standalone.BootstrapDependency[*OCRFactories] = (*proxyDependency)(nil) // Namespace groups the settings under ocr.* (--ocr.proxy-address, CRE_OCR_PROXY_ADDRESS), the same // namespace a hosted peer's settings use: a binary is one or the other, so the names never meet. @@ -72,11 +72,11 @@ func (d *proxyDependency) Dependencies() []standalone.BootstrapCommand { // hop between instances of one process, since the peer this would delegate to is a goroutine beside // it and delegating would mean serialising a message so a gRPC connection to this process could hand // it back. None of this dependency's settings survive into it - see embedded. -func (d *proxyDependency) ForEmbedding(i int) standalone.BootstrapDependency[*Factories] { - return &embedded{lggr: d.lggr, index: i} +func (d *proxyDependency) ForEmbedding(i int) standalone.BootstrapDependency[*OCRFactories] { + return embeddedOCR{&embedded{lggr: d.lggr, index: i}} } -func (d *proxyDependency) Get(context.Context, standalone.CommonConfig) (*Factories, error) { +func (d *proxyDependency) Get(context.Context, standalone.CommonConfig) (*OCRFactories, error) { if d.cfg.ProxyAddress == "" { return nil, errors.New("--ocr.proxy-address is required to delegate rage networking") } @@ -96,20 +96,13 @@ func (d *proxyDependency) Get(context.Context, standalone.CommonConfig) (*Factor _ = endpointFactory.Close() return nil, fmt.Errorf("failed to create proxy OCR3.1 endpoint factory: %w", err) } - pgFactory, err := creproxy.NewProxyPeerGroupFactory(d.cfg.ProxyAddress) - if err != nil { - _ = endpointFactory.Close() - _ = endpoint2Factory.Close() - return nil, fmt.Errorf("failed to create proxy peer group factory: %w", err) - } d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.cfg.ProxyAddress, "peerID", peerID.String()) - return &Factories{ + return &OCRFactories{ OCR2Endpoint: endpointFactory, OCR3_1Endpoint: endpoint2Factory, - PeerGroup: pgFactory, PeerID: peerID, - closer: multiCloser{endpointFactory, endpoint2Factory, pgFactory}, + closer: multiCloser{endpointFactory, endpoint2Factory}, }, nil } diff --git a/libs/x/README.md b/libs/x/README.md new file mode 100644 index 000000000..d4f7c8ff3 --- /dev/null +++ b/libs/x/README.md @@ -0,0 +1,36 @@ +# libs/x + +Code that is passing through on its way somewhere else. Nothing here is a stable API, and nothing +outside `crecore` and chainlink's core should import it. + +Today it holds DON-to-DON communication, moved out of chainlink core so that core and crecore can +build against the same code while crecore takes ownership of it: + +- **`rage`** — the rage p2p transport: a peer, the shared peer that keeps a group and a stream per + remote node, and the types they exchange. Was `core/services/p2p` (+ `core/services/p2p/types`). +- **`don2don`** — everything above the transport: signing, validating and routing messages + (`dispatcher.go`), fanning a call out to every member of a capability DON and agreeing on the + responses (`executable/`), the same in the other direction for triggers, and the transmission + schedule that staggers who is asked when. Was `core/capabilities/remote` (+ + `core/capabilities/transmission` and `core/capabilities/validation`). + +The end state is that crecore owns DON-to-DON outright: a new process talks to another DON by asking +crecore, rather than by running a peer and a dispatcher of its own. When core no longer uses any of +this, it moves into crecore and stops being importable at all. + +The code is kept as close to its origin as possible so the move stays reviewable as a move. What +changed: + +- The dispatcher's configuration is a struct (`don2don.DispatcherConfig`) instead of an interface + onto a node's TOML. +- `rage.SharedPeer` takes a `PeerSource` — a group factory and a peer ID, read at start — instead of + core's `SingletonPeerWrapper`. +- Two small helpers came along rather than dragging their old homes with them: + `rage.NewSignerPeerKeyring` (was in core's `ocrcommon`) and `rage.MustNewPeerID` (was in core's + `utils`). +- The wire protos keep their proto package (`remote`) and field numbers, so only the Go import path + moved. Nothing may generate a second copy: two registrations of `remote.MessageBody` in one binary + panic at init. + +`.golangci.yml` excludes this directory from a few of this repo's rules (including its ban on +go-ethereum), so the code can stay as core wrote it. That exclusion goes away with the package. diff --git a/libs/x/don2don/aggregation/default_mode.go b/libs/x/don2don/aggregation/default_mode.go new file mode 100644 index 000000000..ab292150f --- /dev/null +++ b/libs/x/don2don/aggregation/default_mode.go @@ -0,0 +1,81 @@ +package aggregation + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" +) + +// Default MODE Aggregator needs a configurable number of identical responses for aggregation to succeed +type defaultModeAggregator struct { + minIdenticalResponses uint32 +} + +var _ don2dontypes.Aggregator = &defaultModeAggregator{} + +func NewDefaultModeAggregator(minIdenticalResponses uint32) *defaultModeAggregator { + return &defaultModeAggregator{ + minIdenticalResponses: minIdenticalResponses, + } +} + +func (a *defaultModeAggregator) Aggregate(_ string, responses [][]byte) (commoncap.TriggerResponse, error) { + found, err := AggregateModeRaw(responses, a.minIdenticalResponses) + if err != nil { + return commoncap.TriggerResponse{}, fmt.Errorf("failed to aggregate responses, err: %w", err) + } + + unmarshaled, err := pb.UnmarshalTriggerResponse(found) + if err != nil { + return commoncap.TriggerResponse{}, fmt.Errorf("failed to unmarshal aggregated responses, err: %w", err) + } + return unmarshaled, nil +} + +// AggregateModeRaw finds the mode (most frequent element) in elemList that meets minIdenticalResponses. +// This function is on the critical path of publisher_subscriber.go#Receive() and is called once per workflow per batched message. +// Any change here must be validated by running the benchmark and comparing against the checked-in baseline: +// +// cd ./core/capabilities/remote/aggregation/ +// go test -bench=Benchmark_AggregateModeRaw -benchmem -count=6 . > /tmp/benchmark_new.txt +// benchstat ./testdata/aggregate_mode_raw_benchmark.txt /tmp/benchmark_new.txt +func AggregateModeRaw(elemList [][]byte, minIdenticalResponses uint32) ([]byte, error) { + // Fast path: if elemList[0] reaches majorityThreshold (max(len/2+1, minIdenticalResponses)) hashing won't be needed. + n := len(elemList) + majorityThreshold := max(minIdenticalResponses, uint32(n/2+1)) //nolint:gosec // G115 + var matchCount uint32 = 1 + for i := 1; i < n; i++ { + if bytes.Equal(elemList[0], elemList[i]) { + matchCount++ + if matchCount >= majorityThreshold { + return elemList[0], nil + } + } + } + + // Fallback: elemList[0] is not the majority, use hash-based mode finding. + hashToCount := make(map[[32]byte]uint32, n) + var found []byte + hasher := sha256.New() + for _, elem := range elemList { + hasher.Reset() + hasher.Write(elem) + var key [32]byte + hasher.Sum(key[:0]) + hashToCount[key]++ + if hashToCount[key] >= minIdenticalResponses { + found = elem + minIdenticalResponses = hashToCount[key] + } + } + if found == nil { + return nil, errors.New("not enough identical responses found") + } + return found, nil +} diff --git a/libs/x/don2don/aggregation/default_mode_test.go b/libs/x/don2don/aggregation/default_mode_test.go new file mode 100644 index 000000000..12b58e895 --- /dev/null +++ b/libs/x/don2don/aggregation/default_mode_test.go @@ -0,0 +1,230 @@ +package aggregation + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" +) + +const ( + testPayloadSize = 2000 + testDonSize = 7 + testMinResp = uint32(3) +) + +type aggregationScenario struct { + name string + payloads [][]byte + minResponses uint32 + expectPayload []byte + expectError bool +} + +var ( + triggerEvent1 = map[string]any{"event": "triggerEvent1"} + triggerEvent2 = map[string]any{"event": "triggerEvent2"} +) + +func TestDefaultModeAggregator_Aggregate(t *testing.T) { + t.Parallel() + + val, err := values.NewMap(triggerEvent1) + require.NoError(t, err) + capResponse1 := commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ + Outputs: val, + }, + Err: nil, + } + marshaled1, err := pb.MarshalTriggerResponse(capResponse1) + require.NoError(t, err) + + val2, err := values.NewMap(triggerEvent2) + require.NoError(t, err) + capResponse2 := commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ + Outputs: val2, + }, + Err: nil, + } + marshaled2, err := pb.MarshalTriggerResponse(capResponse2) + require.NoError(t, err) + + agg := NewDefaultModeAggregator(2) + _, err = agg.Aggregate("", [][]byte{marshaled1}) + require.Error(t, err) + + _, err = agg.Aggregate("", [][]byte{marshaled1, marshaled2}) + require.Error(t, err) + + res, err := agg.Aggregate("", [][]byte{marshaled1, marshaled2, marshaled1}) + require.NoError(t, err) + require.Equal(t, res, capResponse1) +} + +func TestAggregateModeRaw_Correctness(t *testing.T) { + t.Parallel() + + for _, sc := range aggregationScenarios(t) { + t.Run(sc.name, func(t *testing.T) { + t.Parallel() + + result, err := AggregateModeRaw(sc.payloads, sc.minResponses) + if sc.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, sc.expectPayload, result) + }) + } +} + +func Benchmark_AggregateModeRaw(b *testing.B) { + for _, sc := range aggregationScenarios(b) { + if sc.expectError { + // not interested in breaking scenarios for benchmarks, only valid ones + continue + } + b.Run(sc.name, func(b *testing.B) { + for b.Loop() { + _, _ = AggregateModeRaw(sc.payloads, sc.minResponses) + } + }) + } +} + +func testPayload(tb testing.TB, data string, eventID string) []byte { + tb.Helper() + val, err := values.NewMap(map[string]any{"event": data}) + require.NoError(tb, err) + resp := commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ID: eventID, Outputs: val}, + } + m, err := pb.MarshalTriggerResponse(resp) + require.NoError(tb, err) + return m +} + +func testDataString(size int, seed int) string { + buf := make([]byte, size) + for i := range buf { + buf[i] = 'A' + byte((seed+i)%26) //nolint:gosec // G115: modulo 26 is always in byte range + } + return string(buf) +} + +// aggregationScenarios returns the shared set of test cases used by both correctness tests and benchmarks. +// If scenarios are added or modified, regenerate the benchmark baseline: +// +// cd ./core/capabilities/remote/aggregation/ +// go test -bench=Benchmark_AggregateModeRaw -benchmem -count=6 . > testdata/aggregate_mode_raw_benchmark.txt +func aggregationScenarios(tb testing.TB) []aggregationScenario { + tb.Helper() + + pA := testPayload(tb, testDataString(testPayloadSize, 0), "event-A") + pB := testPayload(tb, testDataString(testPayloadSize, 13), "event-B") + pC := testPayload(tb, testDataString(testPayloadSize, 7), "event-C") + + nAllTheSame := func() [][]byte { + p := make([][]byte, testDonSize) + for i := range p { + p[i] = pA + } + return p + } + nMixed := func(countA, countB int) [][]byte { + p := make([][]byte, countA+countB) + for i := range countA { + p[i] = pA + } + for i := countA; i < countA+countB; i++ { + p[i] = pB + } + return p + } + + return []aggregationScenario{ + { + name: fmt.Sprintf("%d identical payloads - returns A", testDonSize), + payloads: nAllTheSame(), + minResponses: testMinResp, + expectPayload: pA, + }, + { + name: "3 identical payloads - returns A", + payloads: [][]byte{pA, pA, pA}, + minResponses: testMinResp, + expectPayload: pA, + }, + { + name: fmt.Sprintf("%d payloads: 3×A + %d different - returns A", testDonSize, testDonSize-2), + payloads: func() [][]byte { + p := make([][]byte, testDonSize) + p[0] = pA + p[1] = pA + p[2] = pA + for i := 3; i < testDonSize; i++ { + p[i] = testPayload(tb, testDataString(testPayloadSize, i*7), fmt.Sprintf("diff-%d", i)) + } + return p + }(), + minResponses: testMinResp, + expectPayload: pA, + }, + { + name: fmt.Sprintf("%d payloads: %d×A + %d×B - returns B (mode)", testDonSize, testDonSize/2, testDonSize-testDonSize/2), + payloads: nMixed(testDonSize/2, testDonSize-testDonSize/2), + minResponses: testMinResp, + expectPayload: pB, + }, + { + name: fmt.Sprintf("%d payloads: %d×A + %d×B - returns A (mode)", testDonSize, testDonSize-testDonSize/2, testDonSize/2), + payloads: nMixed(testDonSize-testDonSize/2, testDonSize/2), + minResponses: testMinResp, + expectPayload: pA, + }, + { + name: fmt.Sprintf("%d payloads: %d×A + 1×B - returns A (mode)", testDonSize, testDonSize-1), + payloads: func() [][]byte { + p := make([][]byte, testDonSize) + for i := range testDonSize - 1 { + p[i] = pA + } + p[testDonSize-1] = pB + return p + }(), + minResponses: testMinResp, + expectPayload: pA, + }, + { + name: fmt.Sprintf("%d payloads: [A A B B C C C] - returns C (mode)", testDonSize), + payloads: [][]byte{pA, pA, pB, pB, pC, pC, pC}, + minResponses: testMinResp, + expectPayload: pC, + }, + { + name: fmt.Sprintf("%d payloads: [B B A A C C C] - returns C (mode)", testDonSize), + payloads: [][]byte{pB, pB, pA, pA, pC, pC, pC}, + minResponses: testMinResp, + expectPayload: pC, + }, + { + name: "1 payload with minResponses=2 - error", + payloads: [][]byte{pA}, + minResponses: 2, + expectError: true, + }, + { + name: "2 different payloads with minResponses=2 - error", + payloads: [][]byte{pA, pB}, + minResponses: 2, + expectError: true, + }, + } +} diff --git a/libs/x/don2don/aggregation/signed_report.go b/libs/x/don2don/aggregation/signed_report.go new file mode 100644 index 000000000..70b65eb41 --- /dev/null +++ b/libs/x/don2don/aggregation/signed_report.go @@ -0,0 +1,135 @@ +package aggregation + +import ( + "errors" + "fmt" + "time" + + "google.golang.org/protobuf/proto" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2/types" +) + +var ( + ErrMalformedSigner = errors.New("malformed signer") + ErrMalformedConfig = errors.New("malformed config digest") + ErrInsufficientSignatures = errors.New("insufficient signatures") + ErrMissingResponse = errors.New("missing trigger response") +) + +type signedReportRemoteAggregator struct { + allowedSigners map[common.Address]struct{} + minRequiredSignatures int + maxAgeSec int + capID string + lggr logger.Logger +} + +func NewSignedReportRemoteAggregator(allowedSigners [][]byte, minRequiredSignatures int, capID string, maxAgeSec int, lggr logger.Logger) *signedReportRemoteAggregator { + signersMap := make(map[common.Address]struct{}) + for _, signer := range allowedSigners { + signersMap[common.BytesToAddress(signer)] = struct{}{} + } + lggr = logger.Named(lggr, "SignedReportRemoteAggregator") + + lggr.Infow("created", "allowedSigners", signersMap, "minRequiredSignatures", minRequiredSignatures, "maxAgeSec", maxAgeSec, "capID", capID) + return &signedReportRemoteAggregator{ + allowedSigners: signersMap, + minRequiredSignatures: minRequiredSignatures, + maxAgeSec: maxAgeSec, + capID: capID, + lggr: logger.Named(lggr, "SignedReportRemoteAggregator"), + } +} + +// Accept first response with valid signatures and expected event ID +// every element in responses is must be a [capabilitypb.TriggerResponse] +// and for each of them, we expect the [capabilitypb.TriggerResponse.Event.Outputs] field +// to be the [values.Map] representation a [capabilities.OCRTriggerEvent] (see [capabilities.OCRTriggerEvent.ToMap]) +func (a *signedReportRemoteAggregator) Aggregate(triggerEventID string, responses [][]byte) (capabilities.TriggerResponse, error) { + for _, response := range responses { + triggerResp, err := capabilitiespb.UnmarshalTriggerResponse(response) + if err != nil { + a.lggr.Errorw("could not unmarshal one of capability responses (faulty sender?)", "err", err) + continue + } + ocrEvent := &capabilities.OCRTriggerEvent{} + err = ocrEvent.FromMap(triggerResp.Event.Outputs) + if err != nil { + a.lggr.Errorw("trigger response does not contain an OCR report", "id", triggerResp.Event.ID, "err", err) + } + rawReport := ocrEvent.Report + rep := &capabilitiespb.OCRTriggerReport{} + err = proto.Unmarshal(rawReport, rep) + if err != nil { + a.lggr.Errorw("failed to parse OCR report", "id", triggerResp.Event.ID) + continue + } + + if rep.EventID != triggerEventID { + a.lggr.Warnw("unexpected event ID", "expected", triggerEventID, "got", rep.EventID) + continue + } + + // use Abs to handle edge case of clock skew + timeDiff := time.Since(time.Unix(0, int64(rep.Timestamp))).Abs() //nolint:gosec // disable G115 this won't be running in 2262 + if timeDiff < 0 { + timeDiff = -timeDiff + } + if timeDiff.Nanoseconds() > int64(a.maxAgeSec)*1000000000 { + a.lggr.Warnw("aggregation report too old", "age", timeDiff, "maxAge", a.maxAgeSec, "reportTimestamp", rep.Timestamp) + continue + } + + if err2 := a.validateSignatures(ocrEvent); err2 != nil { + a.lggr.Errorw("invalid signatures", "err", err2) + continue + } + // Replace "Outputs" field with the one extracted from the OCR report and drop the binary report + outputsMap, err := values.FromMapValueProto(rep.Outputs) + if err != nil { + a.lggr.Errorw("failed to parse OCR report outputs", "err", err) + continue + } + triggerResp.Event.Outputs = outputsMap + return triggerResp, nil + } + return capabilities.TriggerResponse{}, fmt.Errorf("%w: %s", ErrMissingResponse, triggerEventID) +} + +func (a *signedReportRemoteAggregator) validateSignatures(event *capabilities.OCRTriggerEvent) error { + digest, err := ocr2types.BytesToConfigDigest(event.ConfigDigest) + if err != nil { + return errors.Join(ErrMalformedConfig, err) + } + fullHash := ocr2key.ReportToSigData3(digest, event.SeqNr, event.Report) + validated := map[common.Address]struct{}{} + for _, sig := range event.Sigs { + signerPubkey, err2 := crypto.SigToPub(fullHash, sig.Signature) + if err2 != nil { + return errors.Join(ErrMalformedSigner, err2) + } + signerAddr := crypto.PubkeyToAddress(*signerPubkey) + if _, ok := a.allowedSigners[signerAddr]; !ok { + a.lggr.Warnw("invalid signer", "signerAddr", signerAddr) + continue + } + validated[signerAddr] = struct{}{} + if len(validated) >= a.minRequiredSignatures { + break // early exit + } + } + if len(validated) < a.minRequiredSignatures { + return fmt.Errorf("%w: got %d, needed %d", ErrInsufficientSignatures, len(validated), a.minRequiredSignatures) + } + return nil +} diff --git a/libs/x/don2don/aggregation/signed_report_test.go b/libs/x/don2don/aggregation/signed_report_test.go new file mode 100644 index 000000000..5b17c7ab7 --- /dev/null +++ b/libs/x/don2don/aggregation/signed_report_test.go @@ -0,0 +1,517 @@ +package aggregation_test + +import ( + "errors" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" + "google.golang.org/protobuf/proto" + + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2/types" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + // "github.com/smartcontractkit/chainlink/common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/aggregation" +) + +func TestSignedReportAggregator_Aggregate(t *testing.T) { + t.Parallel() + + // Setup test keys + kb1, err := ocr2key.New(corekeys.EVM) + require.NoError(t, err) + addr1 := common.BytesToAddress(kb1.PublicKey()) + + kb2, err := ocr2key.New(corekeys.EVM) + require.NoError(t, err) + addr2 := common.BytesToAddress(kb2.PublicKey()) + + kb3, err := ocr2key.New(corekeys.EVM) + require.NoError(t, err) + addr3 := common.BytesToAddress(kb3.PublicKey()) + + // kb4 is an unauthorized signer + kb4, err := ocr2key.New(corekeys.EVM) + require.NoError(t, err) + + // Setup test data + eventID := "test-event-123" + configDigest := ocr2types.ConfigDigest{} + for i := range len(configDigest) { + configDigest[i] = byte(i) + } + seqNr := uint64(123) + currentTime := uint64(time.Now().UnixNano()) + + // Create a valid outputs map + outputsMap, err := values.NewMap(map[string]values.Value{ + "result": values.NewString("success"), + "data": values.NewInt64(42), + }) + require.NoError(t, err) + outputsProto := values.ProtoMap(outputsMap) // outputsMap.ProtoMap() + + // Create a valid OCR report + validReport := &capabilitiespb.OCRTriggerReport{ + EventID: eventID, + Timestamp: currentTime, + Outputs: outputsProto, + } + + reportBytes, err := proto.Marshal(validReport) + require.NoError(t, err) + + sig1, err := kb1.Sign3(configDigest, seqNr, reportBytes) + require.NoError(t, err) + sig2, err := kb2.Sign3(configDigest, seqNr, reportBytes) + require.NoError(t, err) + unauthorizedSig, err := kb4.Sign3(configDigest, seqNr, reportBytes) // Unauthorized signature + require.NoError(t, err) + + // Setup logger + lggr, observedLogs := logger.TestObserved(t, zapcore.WarnLevel) + + // Setup aggregator with 2 required signatures + allowedSigners := [][]byte{ + addr1.Bytes(), + addr2.Bytes(), + addr3.Bytes(), + } + minRequiredSignatures := 2 + maxAgeSec := 10 + capID := "test-capability" + + aggregator := aggregation.NewSignedReportRemoteAggregator( + allowedSigners, + minRequiredSignatures, + capID, + maxAgeSec, + lggr, + ) + + // Test cases + // NOTE: we are checking the logs for errors, so we need to clear the logs before invocation of the aggregator + t.Run("happy path - valid response with enough signatures", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Prepare valid OCR event with enough valid signatures + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: configDigest[:], + SeqNr: seqNr, + Report: reportBytes, + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: sig1}, + {Signature: sig2}, + }, + } + m, err := ocrEvent.ToMap() + require.NoError(t, err) + // Create trigger event with OCR report + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: m, + } + + // Create TriggerResponse + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + // Marshal response + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Expected flattened response + expectedResp := capabilities.TriggerResponse{ + Event: capabilities.TriggerEvent{ + ID: eventID, + Outputs: outputsMap, + }, + } + + // Call Aggregate + clearLogger(t, observedLogs) + result, err := aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.NoError(t, err) + + // Verify response is flattened correctly + assert.Equal(t, expectedResp.Event.ID, result.Event.ID) + assert.Equal(t, expectedResp.Event.Outputs, result.Event.Outputs) + }) + + t.Run("error - unmarshallable response", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Invalid bytes that can't be unmarshalled + invalidBytes := []byte("not a valid response") + + // Call Aggregate with invalid bytes + clearLogger(t, observedLogs) + _, err := aggregator.Aggregate(eventID, [][]byte{invalidBytes}) + require.ErrorIs(t, err, aggregation.ErrMissingResponse) + gotLog := observedLogs.FilterMessage("could not unmarshal one of capability responses (faulty sender?)") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - response without OCR event", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Create event without OCR data + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + // No OCREvent + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with response without OCR event + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("trigger response does not contain an OCR report") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - response with malformed Output map", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Create event with malformed Output map + o, err := values.NewMap(map[string]any{ + "UnexpectedKey": "unexpected value", + }) + require.NoError(t, err) + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: o, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with response without OCR event + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("trigger response does not contain an OCR report") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - unparsable OCR report", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Create an OCR event with invalid report bytes + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: configDigest[:], + SeqNr: seqNr, + Report: []byte("not a valid report"), + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: sig1}, + {Signature: sig2}, + }, + } + m, err := ocrEvent.ToMap() + require.NoError(t, err) + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + + Outputs: m, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with invalid report bytes + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("failed to parse OCR report") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - mismatched event ID", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Create a valid report but with wrong event ID + wrongIDReport := &capabilitiespb.OCRTriggerReport{ + EventID: "wrong-event-id", + Timestamp: currentTime, + Outputs: outputsProto, + } + + wrongReportBytes, err := proto.Marshal(wrongIDReport) + require.NoError(t, err) + + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: configDigest[:], + SeqNr: seqNr, + Report: wrongReportBytes, + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: sig1}, + {Signature: sig2}, + }, + } + m, err := ocrEvent.ToMap() + require.NoError(t, err) + + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: m, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with mismatched event ID + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("unexpected event ID") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - report too old", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Create an old report (beyond maxAgeSec) + oldTime := currentTime - uint64((maxAgeSec+5)*1000000000) //nolint:gosec // disable G115 + + oldReport := &capabilitiespb.OCRTriggerReport{ + EventID: eventID, + Timestamp: oldTime, + Outputs: outputsProto, + } + + oldReportBytes, err := proto.Marshal(oldReport) + require.NoError(t, err) + + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: configDigest[:], + SeqNr: seqNr, + Report: oldReportBytes, + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: sig1}, + {Signature: sig2}, + }, + } + m, err := ocrEvent.ToMap() + require.NoError(t, err) + + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: m, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with old report + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("aggregation report too old") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - not enough valid signatures", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Only one valid signature when two are required + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: configDigest[:], + SeqNr: seqNr, + Report: reportBytes, + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: sig1}, + // Missing second valid signature + }, + } + m, err := ocrEvent.ToMap() + require.NoError(t, err) + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: m, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with not enough valid signatures + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("invalid signatures") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - signatures from unauthorized signers", func(t *testing.T) { //nolint:paralleltest // shared logger state + // One valid signature and one unauthorized + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: configDigest[:], + SeqNr: seqNr, + Report: reportBytes, + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: sig1}, + {Signature: unauthorizedSig}, // Unauthorized signer + }, + } + + m, err := ocrEvent.ToMap() + require.NoError(t, err) + + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: m, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with unauthorized signer + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("invalid signer") + assert.Equal(t, 1, gotLog.Len()) + }) + + t.Run("error - malformed config digest", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Invalid config digest + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: []byte("invalid config digest"), // Wrong length + SeqNr: seqNr, + Report: reportBytes, + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: sig1}, + {Signature: sig2}, + }, + } + + m, err := ocrEvent.ToMap() + require.NoError(t, err) + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: m, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with malformed config digest + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("invalid signatures") + assert.Equal(t, 1, gotLog.Len()) + gotLogLine := gotLog.All()[0] + + assert.True(t, logContainErr(t, gotLogLine, aggregation.ErrMalformedConfig), "expected error to be contained in log") + }) + + t.Run("error - malformed signature", func(t *testing.T) { //nolint:paralleltest // shared logger state + // Invalid signature format + ocrEvent := &capabilities.OCRTriggerEvent{ + ConfigDigest: configDigest[:], + SeqNr: seqNr, + Report: reportBytes, + Sigs: []capabilities.OCRAttributedOnchainSignature{ + {Signature: []byte("invalid signature")}, + {Signature: sig2}, + }, + } + m, err := ocrEvent.ToMap() + require.NoError(t, err) + + triggerEvent := capabilities.TriggerEvent{ + ID: eventID, + Outputs: m, + } + + triggerResp := capabilities.TriggerResponse{ + Event: triggerEvent, + } + + respBytes, err := capabilitiespb.MarshalTriggerResponse(triggerResp) + require.NoError(t, err) + + // Call Aggregate with malformed signature + clearLogger(t, observedLogs) + _, err = aggregator.Aggregate(eventID, [][]byte{respBytes}) + require.Error(t, err) + gotLog := observedLogs.FilterMessage("invalid signatures") + assert.Equal(t, 1, gotLog.Len()) + gotLogLine := gotLog.All()[0] + assert.True(t, logContainErr(t, gotLogLine, aggregation.ErrMalformedSigner), "expected error to be contained in log") + }) +} + +// TODO this seems useful in our common logging package +func logContainErr(t testing.TB, log observer.LoggedEntry, err error) bool { + gotErr := extractErr(t, log) + if gotErr == nil { + t.Logf("expected error, got nil") + return false + } + return errors.Is(gotErr, err) +} + +func extractErr(t testing.TB, log observer.LoggedEntry) error { + t.Helper() + const errKey = "err" + ctxMap := log.ContextMap() + if ctxMap == nil { + t.Logf("expected context map, got %v", log) + return nil + } + if _, ok := ctxMap[errKey]; !ok { + t.Logf("expected %s key in context map, got %v", errKey, ctxMap) + return nil + } + // don't use ctxMap because it erases the type + var f zapcore.Field + for _, c := range log.Context { + if c.Key == errKey { + f = c + break + } + } + gotErr, ok := f.Interface.(error) + if !ok { + t.Logf("expected error type, got %T", ctxMap[errKey]) + return nil + } + return gotErr +} + +func clearLogger(t testing.TB, lggr *observer.ObservedLogs) { + t.Helper() + lggr.TakeAll() +} diff --git a/libs/x/don2don/aggregation/testdata/aggregate_mode_raw_benchmark.txt b/libs/x/don2don/aggregation/testdata/aggregate_mode_raw_benchmark.txt new file mode 100644 index 000000000..1764888dd --- /dev/null +++ b/libs/x/don2don/aggregation/testdata/aggregate_mode_raw_benchmark.txt @@ -0,0 +1,54 @@ +goos: darwin +goarch: arm64 +pkg: github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/aggregation +cpu: Apple M3 Max +Benchmark_AggregateModeRaw/7_identical_payloads_-_returns_A-14 222551481 5.401 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_identical_payloads_-_returns_A-14 219683704 5.551 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_identical_payloads_-_returns_A-14 206173624 5.574 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_identical_payloads_-_returns_A-14 229645402 5.203 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_identical_payloads_-_returns_A-14 225613774 5.238 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_identical_payloads_-_returns_A-14 220228659 5.377 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/3_identical_payloads_-_returns_A-14 252919063 4.426 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/3_identical_payloads_-_returns_A-14 283354431 4.119 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/3_identical_payloads_-_returns_A-14 298910098 4.017 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/3_identical_payloads_-_returns_A-14 298851042 4.044 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/3_identical_payloads_-_returns_A-14 287628613 4.116 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/3_identical_payloads_-_returns_A-14 279908298 4.241 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_5_different_-_returns_A-14 207847 5471 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_5_different_-_returns_A-14 217500 5343 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_5_different_-_returns_A-14 232759 5464 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_5_different_-_returns_A-14 233397 5417 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_5_different_-_returns_A-14 227596 5301 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_5_different_-_returns_A-14 238802 5284 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_4×B_-_returns_B_(mode)-14 215630 5497 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_4×B_-_returns_B_(mode)-14 216494 5475 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_4×B_-_returns_B_(mode)-14 230018 5545 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_4×B_-_returns_B_(mode)-14 213571 5431 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_4×B_-_returns_B_(mode)-14 225447 5365 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_3×A_+_4×B_-_returns_B_(mode)-14 225266 5475 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_4×A_+_3×B_-_returns_A_(mode)-14 232269152 5.201 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_4×A_+_3×B_-_returns_A_(mode)-14 228310303 5.226 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_4×A_+_3×B_-_returns_A_(mode)-14 235422214 5.094 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_4×A_+_3×B_-_returns_A_(mode)-14 225712828 5.418 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_4×A_+_3×B_-_returns_A_(mode)-14 223602483 5.269 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_4×A_+_3×B_-_returns_A_(mode)-14 221815747 5.520 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_6×A_+_1×B_-_returns_A_(mode)-14 220471143 5.377 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_6×A_+_1×B_-_returns_A_(mode)-14 220703421 5.373 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_6×A_+_1×B_-_returns_A_(mode)-14 225019899 5.429 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_6×A_+_1×B_-_returns_A_(mode)-14 228841460 5.318 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_6×A_+_1×B_-_returns_A_(mode)-14 234572863 5.154 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_6×A_+_1×B_-_returns_A_(mode)-14 229786855 5.188 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[A_A_B_B_C_C_C]_-_returns_C_(mode)-14 228939 5342 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[A_A_B_B_C_C_C]_-_returns_C_(mode)-14 232429 5210 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[A_A_B_B_C_C_C]_-_returns_C_(mode)-14 230918 5248 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[A_A_B_B_C_C_C]_-_returns_C_(mode)-14 232554 5183 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[A_A_B_B_C_C_C]_-_returns_C_(mode)-14 233356 5261 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[A_A_B_B_C_C_C]_-_returns_C_(mode)-14 228063 5267 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[B_B_A_A_C_C_C]_-_returns_C_(mode)-14 232585 5118 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[B_B_A_A_C_C_C]_-_returns_C_(mode)-14 239976 5082 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[B_B_A_A_C_C_C]_-_returns_C_(mode)-14 231559 5094 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[B_B_A_A_C_C_C]_-_returns_C_(mode)-14 236022 5266 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[B_B_A_A_C_C_C]_-_returns_C_(mode)-14 230274 5320 ns/op 0 B/op 0 allocs/op +Benchmark_AggregateModeRaw/7_payloads:_[B_B_A_A_C_C_C]_-_returns_C_(mode)-14 227611 5370 ns/op 0 B/op 0 allocs/op +PASS +ok github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/aggregation 58.447s diff --git a/libs/x/don2don/combined_client.go b/libs/x/don2don/combined_client.go new file mode 100644 index 000000000..5ebf2c3f4 --- /dev/null +++ b/libs/x/don2don/combined_client.go @@ -0,0 +1,120 @@ +package don2don + +import ( + "context" + "fmt" + "sync" + + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" +) + +// CombinedClient represents a remote capability V2 accessed from a local node (by the Engine). +// The capability can have multiple methods, each one being a trigger or an executable. +// The CombinedClient holds method-specific shims for each method and forwards capability API calls +// to them. Responses are passed directly to method-specific shims from the Dispatcher. +type CombinedClient interface { + capabilities.ExecutableAndTriggerCapability + SetTriggerSubscriber(method string, subscriber capabilities.TriggerCapability) + SetExecutableClient(method string, client capabilities.ExecutableCapability) + GetTriggerSubscriber(method string) capabilities.TriggerCapability + GetExecutableClient(method string) capabilities.ExecutableCapability +} + +type combinedClient struct { + info capabilities.CapabilityInfo + triggerSubscribers map[string]capabilities.TriggerCapability + executableClients map[string]capabilities.ExecutableCapability + mu sync.RWMutex +} + +var _ CombinedClient = &combinedClient{} + +func (c *combinedClient) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { + return c.info, nil +} + +func (c *combinedClient) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + c.mu.RLock() + subscriber, ok := c.triggerSubscribers[request.Method] + c.mu.RUnlock() + + if !ok { + return nil, fmt.Errorf("method %s not defined", request.Method) + } + return subscriber.RegisterTrigger(ctx, request) +} + +func (c *combinedClient) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error { + c.mu.RLock() + subscriber, ok := c.triggerSubscribers[request.Method] + c.mu.RUnlock() + + if !ok { + return fmt.Errorf("method %s not defined", request.Method) + } + return subscriber.UnregisterTrigger(ctx, request) +} + +func (c *combinedClient) AckEvent(ctx context.Context, triggerID string, eventID string, method string) error { + c.mu.RLock() + trigger, ok := c.triggerSubscribers[method] + c.mu.RUnlock() + + if !ok { + return fmt.Errorf("method %s not defined", method) + } + return trigger.AckEvent(ctx, triggerID, eventID, method) +} + +func (c *combinedClient) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return errors.New("RegisterToWorkflow is not supported by remote capabilities") +} + +func (c *combinedClient) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return errors.New("UnregisterFromWorkflow is not supported by remote capabilities") +} + +func (c *combinedClient) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + c.mu.RLock() + client, ok := c.executableClients[request.Method] + c.mu.RUnlock() + + if !ok { + return capabilities.CapabilityResponse{}, fmt.Errorf("method %s not defined", request.Method) + } + return client.Execute(ctx, request) +} + +func NewCombinedClient(info capabilities.CapabilityInfo) *combinedClient { + return &combinedClient{ + info: info, + triggerSubscribers: make(map[string]capabilities.TriggerCapability), + executableClients: make(map[string]capabilities.ExecutableCapability), + } +} + +func (c *combinedClient) SetTriggerSubscriber(method string, subscriber capabilities.TriggerCapability) { + c.mu.Lock() + defer c.mu.Unlock() + c.triggerSubscribers[method] = subscriber +} + +func (c *combinedClient) SetExecutableClient(method string, client capabilities.ExecutableCapability) { + c.mu.Lock() + defer c.mu.Unlock() + c.executableClients[method] = client +} + +func (c *combinedClient) GetTriggerSubscriber(method string) capabilities.TriggerCapability { + c.mu.RLock() + defer c.mu.RUnlock() + return c.triggerSubscribers[method] +} + +func (c *combinedClient) GetExecutableClient(method string) capabilities.ExecutableCapability { + c.mu.RLock() + defer c.mu.RUnlock() + return c.executableClients[method] +} diff --git a/libs/x/don2don/config.go b/libs/x/don2don/config.go new file mode 100644 index 000000000..b610628bf --- /dev/null +++ b/libs/x/don2don/config.go @@ -0,0 +1,24 @@ +package don2don + +// DispatcherConfig is what the dispatcher needs to know about itself. It is a struct rather than +// the interface core declares in core/config, because a config here is a value a binary fills in +// (from flags, in the standalone framework) rather than a view onto a node's TOML. +type DispatcherConfig struct { + // SupportedVersion is stamped on every outgoing message. + SupportedVersion int + // ReceiverBufferSize is how many messages may queue for one receiver before they are dropped. + ReceiverBufferSize int + // RateLimit caps inbound traffic, in total and per sender. + RateLimit DispatcherRateLimit + // SendToSharedPeer routes outgoing messages through the DON-to-DON shared peer rather than the + // legacy external peer. + SendToSharedPeer bool +} + +// DispatcherRateLimit caps how much inbound traffic is accepted. +type DispatcherRateLimit struct { + GlobalRPS float64 + GlobalBurst int + PerSenderRPS float64 + PerSenderBurst int +} diff --git a/libs/x/don2don/dispatcher.go b/libs/x/don2don/dispatcher.go new file mode 100644 index 000000000..e8d872516 --- /dev/null +++ b/libs/x/don2don/dispatcher.go @@ -0,0 +1,402 @@ +package don2don + +import ( + "context" + "fmt" + "strconv" + "sync" + "time" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/ratelimit" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +var ( + ErrReceiverExists = errors.New("receiver already exists") +) + +// dispatcher en/decodes messages and routes traffic between peers and capabilities +type dispatcher struct { + cfg DispatcherConfig + peerWrapper rage.PeerWrapper + peer rage.Peer + peerID rage.PeerID + signer rage.Signer + don2donSharedPeer rage.SharedPeer + registry core.CapabilitiesRegistry + rateLimiter *ratelimit.RateLimiter + receivers map[key]*receiver + mu sync.RWMutex + stopCh services.StopChan + wg sync.WaitGroup + lggr logger.Logger + + metrics dispatcherMetrics +} + +type dispatcherMetrics struct { + externalPeerMsgsRcvdCounter metric.Int64Counter + sharedPeerMsgsRcvdCounter metric.Int64Counter + rateLimitedMsgsCounter metric.Int64Counter + invalidMsgsCounter metric.Int64Counter + unknownCapMsgsCounter metric.Int64Counter + receiveChannelUsageGauge metric.Float64Gauge + receiverDroppedMsgsCounter metric.Int64Counter + messagesSentCounter metric.Int64Counter + sendErrorsCounter metric.Int64Counter +} + +var _ types.Dispatcher = &dispatcher{} + +type key struct { + capID string + donID uint32 + methodName string +} + +var _ services.Service = &dispatcher{} + +func NewDispatcher(cfg DispatcherConfig, peerWrapper rage.PeerWrapper, don2donSharedPeer rage.SharedPeer, signer rage.Signer, registry core.CapabilitiesRegistry, lggr logger.Logger) (*dispatcher, error) { + rl, err := ratelimit.NewRateLimiter(ratelimit.RateLimiterConfig{ + GlobalRPS: cfg.RateLimit.GlobalRPS, + GlobalBurst: cfg.RateLimit.GlobalBurst, + PerSenderRPS: cfg.RateLimit.PerSenderRPS, + PerSenderBurst: cfg.RateLimit.PerSenderBurst, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to create rate limiter") + } + return &dispatcher{ + cfg: cfg, + peerWrapper: peerWrapper, + signer: signer, + registry: registry, + rateLimiter: rl, + receivers: make(map[key]*receiver), + stopCh: make(services.StopChan), + lggr: logger.Named(lggr, "Dispatcher"), + don2donSharedPeer: don2donSharedPeer, + }, nil +} + +func (d *dispatcher) initMetrics() error { + var err error + d.metrics.externalPeerMsgsRcvdCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_external_peer_msgs_rcvd_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_external_peer_msgs_rcvd_total): %w", err) + } + d.metrics.sharedPeerMsgsRcvdCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_shared_peer_msgs_rcvd_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_shared_peer_msgs_rcvd_total): %w", err) + } + d.metrics.rateLimitedMsgsCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_rate_limited_msgs_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_rate_limited_msgs_total: %w", err) + } + d.metrics.invalidMsgsCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_invalid_msgs_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_invalid_msgs_total: %w", err) + } + d.metrics.unknownCapMsgsCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_unknown_capability_msgs_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_unknown_capability_msgs_total: %w", err) + } + d.metrics.receiveChannelUsageGauge, err = beholder.GetMeter().Float64Gauge("platform_don2don_dispatcher_receive_channel_usage") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_receive_channel_usage: %w", err) + } + d.metrics.receiverDroppedMsgsCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_receiver_dropped_msgs_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_receiver_dropped_msgs_total: %w", err) + } + d.metrics.messagesSentCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_messages_sent_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_messages_sent_total: %w", err) + } + d.metrics.sendErrorsCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_dispatcher_send_errors_total") + if err != nil { + return fmt.Errorf("failed to register platform_don2don_dispatcher_send_errors_total: %w", err) + } + return nil +} + +func (d *dispatcher) Start(ctx context.Context) error { + if d.peerWrapper == nil && d.don2donSharedPeer == nil { + return errors.New("either peerWrapper or don2donSharedPeer must be set") + } + if d.peerWrapper != nil { + d.peer = d.peerWrapper.GetPeer() + d.peerID = d.peer.ID() + if d.peer == nil { + return errors.New("peer is not initialized") + } + } + if d.don2donSharedPeer != nil { + if (d.peerID != rage.PeerID{}) && d.peerID != d.don2donSharedPeer.ID() { + return errors.New("peer ID from peerWrapper and don2donSharedPeer do not match") + } + d.peerID = d.don2donSharedPeer.ID() + } + err := d.signer.Initialize() + if err != nil { + return errors.Wrap(err, "failed to initialize signer") + } + if err = d.initMetrics(); err != nil { + return errors.Wrap(err, "failed to initialize metrics") + } + d.wg.Go(func() { + d.receive() + }) + + d.lggr.Info("dispatcher started") + return nil +} + +func (d *dispatcher) Close() error { + close(d.stopCh) + d.wg.Wait() + d.lggr.Info("dispatcher closed") + return nil +} + +type receiver struct { + cancel context.CancelFunc + ch chan *types.MessageBody +} + +func (d *dispatcher) SetReceiverForMethod(capabilityID string, donID uint32, method string, rec types.Receiver) error { + return d.setReceiver(key{capabilityID, donID, method}, rec) +} + +func (d *dispatcher) SetReceiver(capabilityID string, donID uint32, rec types.Receiver) error { + return d.setReceiver(key{capabilityID, donID, ""}, rec) // empty method name +} + +func (d *dispatcher) setReceiver(k key, rec types.Receiver) error { + d.mu.Lock() + defer d.mu.Unlock() + _, ok := d.receivers[k] + if ok { + return fmt.Errorf("%w: receiver already exists for capability %s, donID %d, method %s", ErrReceiverExists, k.capID, k.donID, k.methodName) + } + receiverCh := make(chan *types.MessageBody, d.cfg.ReceiverBufferSize) + + ctx, cancelCtx := d.stopCh.NewCtx() + d.wg.Add(1) + go func() { + defer cancelCtx() + defer d.wg.Done() + for { + select { + case <-ctx.Done(): + return + case msg := <-receiverCh: + func() { + defer func() { + if r := recover(); r != nil { + d.lggr.Errorw("Recovered goroutine panic", + "panic", r, + "capabilityId", SanitizeLogString(k.capID), + "donId", k.donID, + "methodName", k.methodName, + "msgMethod", msg.Method, + "sender", msg.Sender, + ) + } + }() + rec.Receive(ctx, msg) + }() + } + } + }() + + d.receivers[k] = &receiver{ + cancel: cancelCtx, + ch: receiverCh, + } + + d.lggr.Debugw("receiver set", "capabilityId", k.capID, "donId", k.donID, "methodName", k.methodName) + return nil +} + +func (d *dispatcher) RemoveReceiverForMethod(capabilityID string, donID uint32, method string) { + d.removeReceiver(key{capabilityID, donID, method}) +} + +func (d *dispatcher) RemoveReceiver(capabilityID string, donID uint32) { + d.removeReceiver(key{capabilityID, donID, ""}) // empty method name +} + +func (d *dispatcher) removeReceiver(k key) { + d.mu.Lock() + defer d.mu.Unlock() + if receiver, ok := d.receivers[k]; ok { + // NOTE: receiver.ch is not drained or closed - handle it if receivers are ever dynamically removed/re-added. + receiver.cancel() + delete(d.receivers, k) + d.lggr.Debugw("receiver removed", "capabilityId", k.capID, "donId", k.donID, "methodName", k.methodName) + } +} + +func (d *dispatcher) Send(peerID rage.PeerID, msgBody *types.MessageBody) error { + //nolint:gosec // disable G115 + msgBody.Version = uint32(d.cfg.SupportedVersion) + msgBody.Sender = d.peerID[:] + msgBody.Receiver = peerID[:] + msgBody.Timestamp = time.Now().UnixMilli() + rawBody, err := proto.Marshal(msgBody) + if err != nil { + return err + } + signature, err := d.signer.Sign(rawBody) + if err != nil { + return err + } + msg := &types.Message{Signature: signature, Body: rawBody} + rawMsg, err := proto.Marshal(msg) + if err != nil { + return err + } + + var sendErr error + switch { + case d.cfg.SendToSharedPeer: + sendErr = d.don2donSharedPeer.Send(peerID, rawMsg) + case d.peer != nil: + sendErr = d.peer.Send(peerID, rawMsg) + default: + sendErr = errors.New("no peer available to send message") + } + + ctx, cancel := d.stopCh.NewCtx() + defer cancel() + methodAttr := attribute.String("method", msgBody.Method) + if sendErr != nil { + d.metrics.sendErrorsCounter.Add(ctx, 1, metric.WithAttributes(methodAttr)) + return sendErr + } + d.metrics.messagesSentCounter.Add(ctx, 1, metric.WithAttributes(methodAttr)) + return nil +} + +func (d *dispatcher) receive() { + externalPeerRecvCh := make(<-chan rage.Message) + if d.peer != nil { + externalPeerRecvCh = d.peer.Receive() + } + sharedPeerRecvCh := make(<-chan rage.Message) + if d.don2donSharedPeer != nil { + sharedPeerRecvCh = d.don2donSharedPeer.Receive() + } + ctx, cancel := d.stopCh.NewCtx() + defer cancel() + for { + select { + case <-d.stopCh: + d.lggr.Info("stopped - exiting receive") + return + case msg := <-externalPeerRecvCh: // deprecated, will be removed in favor of SharedPeer (CRE-707) + d.metrics.externalPeerMsgsRcvdCounter.Add(ctx, 1) + d.handleMessage(ctx, &msg) + case msg, ok := <-sharedPeerRecvCh: + if !ok { + d.lggr.Info("shared peer channel closed - exiting receive") + return + } + d.metrics.sharedPeerMsgsRcvdCounter.Add(ctx, 1) + d.handleMessage(ctx, &msg) + } + } +} + +func (d *dispatcher) handleMessage(ctx context.Context, msg *rage.Message) { + sender := msg.Sender.String() + if !d.rateLimiter.Allow(sender) { + d.metrics.rateLimitedMsgsCounter.Add(ctx, 1, metric.WithAttributes( + attribute.String("sender", sender), + )) + d.lggr.Errorw("rate limit exceeded, dropping message", "sender", msg.Sender) + return + } + body, err := ValidateMessage(msg, d.peerID) + if err != nil { + d.metrics.invalidMsgsCounter.Add(ctx, 1, metric.WithAttributes( + attribute.String("sender", sender), + )) + d.lggr.Debugw("received invalid message", "error", err) + d.tryRespondWithError(msg.Sender, body, types.Error_VALIDATION_FAILED) + return + } + // CapabilityMethod will be empty for legacy "v1" messages + k := key{body.CapabilityId, body.CapabilityDonId, body.CapabilityMethod} + d.mu.RLock() + receiver, ok := d.receivers[k] + d.mu.RUnlock() + if !ok { + d.metrics.unknownCapMsgsCounter.Add(ctx, 1, metric.WithAttributes( + attribute.String("capabilityId", k.capID), + attribute.String("donId", strconv.FormatUint(uint64(k.donID), 10)), + attribute.String("method", k.methodName), + )) + d.lggr.Debugw("received message for unknown capability or method", "capabilityId", SanitizeLogString(k.capID), "donId", k.donID, "method", k.methodName) + d.tryRespondWithError(msg.Sender, body, types.Error_CAPABILITY_NOT_FOUND) + return + } + + receiverQueueUsage := float64(0) + if d.cfg.ReceiverBufferSize > 0 { + receiverQueueUsage = float64(len(receiver.ch)) / float64(d.cfg.ReceiverBufferSize) + } + capAttrs := metric.WithAttributes( + attribute.String("capabilityId", k.capID), + attribute.String("donId", strconv.FormatUint(uint64(k.donID), 10)), + attribute.String("method", k.methodName), + ) + d.metrics.receiveChannelUsageGauge.Record(ctx, receiverQueueUsage, capAttrs) + select { + case receiver.ch <- body: + default: + d.metrics.receiverDroppedMsgsCounter.Add(ctx, 1, capAttrs) + d.lggr.Errorw("receiver channel full, dropping message", "capabilityId", k.capID, "donId", k.donID) + } +} + +func (d *dispatcher) tryRespondWithError(peerID rage.PeerID, body *types.MessageBody, errType types.Error) { + if body == nil { + return + } + if body.Error != types.Error_OK { + d.lggr.Debug("received an invalid message with error field set - not responding to avoid an infinite loop") + return + } + body.Error = errType + // clear payload to reduce message size + body.Payload = nil + err := d.Send(peerID, body) + if err != nil { + d.lggr.Debugw("failed to send error response", "error", err) + } +} + +func (d *dispatcher) Ready() error { + return nil +} + +func (d *dispatcher) HealthReport() map[string]error { + return nil +} + +func (d *dispatcher) Name() string { + return d.lggr.Name() +} diff --git a/libs/x/don2don/dispatcher_test.go b/libs/x/don2don/dispatcher_test.go new file mode 100644 index 000000000..c8d607a58 --- /dev/null +++ b/libs/x/don2don/dispatcher_test.go @@ -0,0 +1,339 @@ +package don2don_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" + "github.com/smartcontractkit/capabilities/libs/x/rage/mocks" + + commonMocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" +) + +type testReceiver struct { + ch chan *don2dontypes.MessageBody +} + +func newReceiver() *testReceiver { + return &testReceiver{ + ch: make(chan *don2dontypes.MessageBody, 100), + } +} + +func (r *testReceiver) Receive(_ context.Context, msg *don2dontypes.MessageBody) { + r.ch <- msg +} + +func TestDispatcher_CleanStartClose(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + ctx := t.Context() + peer := mocks.NewPeer(t) + recvCh := make(<-chan rage.Message) + peer.On("Receive", mock.Anything).Return(recvCh) + peer.On("ID", mock.Anything).Return(rage.PeerID{}) + wrapper := mocks.NewPeerWrapper(t) + wrapper.On("GetPeer").Return(peer) + signer := mocks.NewSigner(t) + signer.EXPECT().Initialize().Return(nil) + registry := commonMocks.NewCapabilitiesRegistry(t) + + dispatcher, err := don2don.NewDispatcher(newTestConfig(false), wrapper, nil, signer, registry, lggr) + require.NoError(t, err) + require.NoError(t, dispatcher.Start(ctx)) + require.NoError(t, dispatcher.Close()) +} + +func TestDispatcher_Receive(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + ctx := t.Context() + privKey1, peerID1 := newKeyPair(t) + _, peerID2 := newKeyPair(t) + + peer := mocks.NewPeer(t) + recvCh := make(chan rage.Message) + peer.On("Receive", mock.Anything).Return((<-chan rage.Message)(recvCh)) + peer.On("ID", mock.Anything).Return(peerID2) + wrapper := mocks.NewPeerWrapper(t) + wrapper.On("GetPeer").Return(peer) + signer := mocks.NewSigner(t) + signer.EXPECT().Initialize().Return(nil) + signer.EXPECT().Sign(mock.Anything).Return(nil, errors.New("not implemented")) + registry := commonMocks.NewCapabilitiesRegistry(t) + + dispatcher, err := don2don.NewDispatcher(newTestConfig(false), wrapper, nil, signer, registry, lggr) + require.NoError(t, err) + require.NoError(t, dispatcher.Start(ctx)) + + rcv := newReceiver() + err = dispatcher.SetReceiver(capID1, donID1, rcv) + require.NoError(t, err) + + // supported capability + recvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload1)) + // unknown capability + recvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID2, donID1, []byte(payload1)) + // sender doesn't match + invalid := encodeAndSign(t, privKey1, peerID1, peerID2, capID2, donID1, []byte(payload1)) + invalid.Sender = peerID2 + recvCh <- invalid + // supported capability again + recvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload2)) + + m := <-rcv.ch + require.Equal(t, payload1, string(m.Payload)) + m = <-rcv.ch + require.Equal(t, payload2, string(m.Payload)) + + dispatcher.RemoveReceiver(capID1, donID1) + require.NoError(t, dispatcher.Close()) +} + +func TestDispatcher_ReceiveForMethod(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + ctx := t.Context() + privKey1, peerID1 := newKeyPair(t) + _, peerID2 := newKeyPair(t) + + peer := mocks.NewPeer(t) + recvCh := make(chan rage.Message) + peer.On("Receive", mock.Anything).Return((<-chan rage.Message)(recvCh)) + peer.On("ID", mock.Anything).Return(peerID2) + wrapper := mocks.NewPeerWrapper(t) + wrapper.On("GetPeer").Return(peer) + signer := mocks.NewSigner(t) + signer.EXPECT().Initialize().Return(nil) + signer.EXPECT().Sign(mock.Anything).Return(nil, errors.New("not implemented")) + registry := commonMocks.NewCapabilitiesRegistry(t) + + dispatcher, err := don2don.NewDispatcher(newTestConfig(false), wrapper, nil, signer, registry, lggr) + require.NoError(t, err) + require.NoError(t, dispatcher.Start(ctx)) + + methodA, methodB := "methodA", "methodB" + rcvA, rcvB := newReceiver(), newReceiver() + require.NoError(t, dispatcher.SetReceiverForMethod(capID1, donID1, methodA, rcvA)) + require.NoError(t, dispatcher.SetReceiverForMethod(capID1, donID1, methodB, rcvB)) + + // supported capability / methodA + recvCh <- encodeAndSignForMethod(t, privKey1, peerID1, peerID2, capID1, methodA, donID1, []byte(payload1)) + // unknown capability + recvCh <- encodeAndSignForMethod(t, privKey1, peerID1, peerID2, capID2, methodA, donID1, []byte(payload1)) + // supported capability / methodB + recvCh <- encodeAndSignForMethod(t, privKey1, peerID1, peerID2, capID1, methodB, donID1, []byte(payload2)) + + m := <-rcvA.ch + require.Equal(t, payload1, string(m.Payload)) + m = <-rcvB.ch + require.Equal(t, payload2, string(m.Payload)) + + dispatcher.RemoveReceiverForMethod(capID1, donID1, methodA) + dispatcher.RemoveReceiverForMethod(capID1, donID1, methodB) + require.NoError(t, dispatcher.Close()) +} + +func TestDispatcher_RespondWithError(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + ctx := t.Context() + privKey1, peerID1 := newKeyPair(t) + _, peerID2 := newKeyPair(t) + + peer := mocks.NewPeer(t) + recvCh := make(chan rage.Message) + peer.On("Receive", mock.Anything).Return((<-chan rage.Message)(recvCh)) + peer.On("ID", mock.Anything).Return(peerID2) + sendCh := make(chan rage.PeerID) + peer.On("Send", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + peerID := args.Get(0).(rage.PeerID) + sendCh <- peerID + }).Return(nil) + wrapper := mocks.NewPeerWrapper(t) + wrapper.On("GetPeer").Return(peer) + signer := mocks.NewSigner(t) + signer.EXPECT().Initialize().Return(nil) + signer.EXPECT().Sign(mock.Anything).Return([]byte{1, 2, 3}, nil) + registry := commonMocks.NewCapabilitiesRegistry(t) + + dispatcher, err := don2don.NewDispatcher(newTestConfig(false), wrapper, nil, signer, registry, lggr) + require.NoError(t, err) + require.NoError(t, dispatcher.Start(ctx)) + + // unknown capability + recvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload1)) + responseDestPeerID := <-sendCh + require.Equal(t, peerID1, responseDestPeerID) + + require.NoError(t, dispatcher.Close()) +} + +func TestDispatcher_ReceiveFromBothPeers(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + ctx := t.Context() + privKey1, peerID1 := newKeyPair(t) + _, peerID2 := newKeyPair(t) + + peer := mocks.NewPeer(t) + recvCh := make(chan rage.Message) + peer.On("Receive", mock.Anything).Return((<-chan rage.Message)(recvCh)) + peer.On("ID", mock.Anything).Return(peerID2) + wrapper := mocks.NewPeerWrapper(t) + wrapper.On("GetPeer").Return(peer) + signer := mocks.NewSigner(t) + signer.EXPECT().Initialize().Return(nil) + sharedPeer := mocks.NewSharedPeer(t) + sharedPeerRecvCh := make(chan rage.Message) + sharedPeer.On("Receive", mock.Anything).Return((<-chan rage.Message)(sharedPeerRecvCh)) + sharedPeer.On("ID", mock.Anything).Return(peerID2) + registry := commonMocks.NewCapabilitiesRegistry(t) + + dispatcher, err := don2don.NewDispatcher(newTestConfig(false), wrapper, sharedPeer, signer, registry, lggr) + require.NoError(t, err) + require.NoError(t, dispatcher.Start(ctx)) + + rcv := newReceiver() + err = dispatcher.SetReceiver(capID1, donID1, rcv) + require.NoError(t, err) + + recvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload1)) + sharedPeerRecvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload2)) + close(sharedPeerRecvCh) // make sure Dispatcher handles SharedPeer shutdown gracefully + + m := <-rcv.ch + require.Equal(t, payload1, string(m.Payload)) + m = <-rcv.ch + require.Equal(t, payload2, string(m.Payload)) + + dispatcher.RemoveReceiver(capID1, donID1) + require.NoError(t, dispatcher.Close()) +} + +func TestDispatcher_SendToSharedPeer(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + ctx := t.Context() + _, peerID1 := newKeyPair(t) + _, peerID2 := newKeyPair(t) + + peer := mocks.NewPeer(t) + recvCh := make(chan rage.Message) + peer.On("Receive", mock.Anything).Return((<-chan rage.Message)(recvCh)) + peer.On("ID", mock.Anything).Return(peerID2) + wrapper := mocks.NewPeerWrapper(t) + wrapper.On("GetPeer").Return(peer) + signer := mocks.NewSigner(t) + signer.EXPECT().Initialize().Return(nil) + signer.EXPECT().Sign(mock.Anything).Return([]byte("signed payload"), nil) + sharedPeer := mocks.NewSharedPeer(t) + sharedPeerRecvCh := make(chan rage.Message) + sharedPeer.On("Receive", mock.Anything).Return((<-chan rage.Message)(sharedPeerRecvCh)) + sharedPeer.On("ID", mock.Anything).Return(peerID2) + sharedPeer.On("Send", mock.Anything, mock.Anything).Return(nil) + registry := commonMocks.NewCapabilitiesRegistry(t) + + dispatcher, err := don2don.NewDispatcher(newTestConfig(true), wrapper, sharedPeer, signer, registry, lggr) + require.NoError(t, err) + require.NoError(t, dispatcher.Start(ctx)) + + require.NoError(t, dispatcher.Send(peerID1, &don2dontypes.MessageBody{})) + // mocks expect Sign() and Send() + + require.NoError(t, dispatcher.Close()) +} + +// panicOnFirstReceiver panics on the first Receive call and records all +// subsequent messages so tests can assert the goroutine survived. +type panicOnFirstReceiver struct { + mu sync.Mutex + calls int + received chan *don2dontypes.MessageBody +} + +func newPanicOnFirstReceiver() *panicOnFirstReceiver { + return &panicOnFirstReceiver{received: make(chan *don2dontypes.MessageBody, 10)} +} + +func (r *panicOnFirstReceiver) Receive(_ context.Context, msg *don2dontypes.MessageBody) { + r.mu.Lock() + r.calls++ + call := r.calls + r.mu.Unlock() + if call == 1 { + panic("deliberate panic from receiver") + } + r.received <- msg +} + +// TestDispatcher_ReceiverPanicDoesNotKillLoop verifies that a panic inside a +// receiver's Receive() method is caught by the recover() wrapper in the +// dispatcher goroutine and does not prevent subsequent messages from being +// delivered to the same receiver. +func TestDispatcher_ReceiverPanicDoesNotKillLoop(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + ctx := t.Context() + privKey1, peerID1 := newKeyPair(t) + _, peerID2 := newKeyPair(t) + + peer := mocks.NewPeer(t) + recvCh := make(chan rage.Message) + peer.On("Receive", mock.Anything).Return((<-chan rage.Message)(recvCh)) + peer.On("ID", mock.Anything).Return(peerID2) + wrapper := mocks.NewPeerWrapper(t) + wrapper.On("GetPeer").Return(peer) + signer := mocks.NewSigner(t) + signer.EXPECT().Initialize().Return(nil) + registry := commonMocks.NewCapabilitiesRegistry(t) + + dispatcher, err := don2don.NewDispatcher(newTestConfig(false), wrapper, nil, signer, registry, lggr) + require.NoError(t, err) + require.NoError(t, dispatcher.Start(ctx)) + + rcv := newPanicOnFirstReceiver() + err = dispatcher.SetReceiver(capID1, donID1, rcv) + require.NoError(t, err) + + // First message triggers the panic; the goroutine must survive. + recvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload1)) + // Second message must still be delivered. + recvCh <- encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload2)) + + m := <-rcv.received + require.Equal(t, payload2, string(m.Payload)) + + dispatcher.RemoveReceiver(capID1, donID1) + require.NoError(t, dispatcher.Close()) +} + +func newTestConfig(sendToSharedPeer bool) don2don.DispatcherConfig { + return don2don.DispatcherConfig{ + SupportedVersion: 1, + ReceiverBufferSize: 10000, + RateLimit: don2don.DispatcherRateLimit{ + GlobalRPS: 800.0, + GlobalBurst: 100, + PerSenderRPS: 10.0, + PerSenderBurst: 50, + }, + SendToSharedPeer: sendToSharedPeer, + } +} diff --git a/libs/x/don2don/doc.go b/libs/x/don2don/doc.go new file mode 100644 index 000000000..a46f85e04 --- /dev/null +++ b/libs/x/don2don/doc.go @@ -0,0 +1,30 @@ +// Package don2don is DON-to-DON communication: sending a request to every member of another DON, +// collecting their answers, and agreeing on one. +// +// The pieces, from the bottom up: +// +// - dispatcher.go signs, validates and routes messages. It is the seam the rest of this package +// sits on: everything above it says "send this to that peer ID" and knows nothing about how. +// - executable/ turns one call into a request to each member of the capability DON, counts +// identical responses until F+1 agree, and fails early once agreement has become impossible. +// Its mirror on the receiving side waits for a quorum of callers before doing any work. +// - trigger_publisher.go and trigger_subscriber.go do the same in the other direction, for +// capabilities that emit rather than answer. +// - transmission/ decides who is asked when, so a DON does not stampede a capability. +// +// None of it is tied to rage. A dispatcher needs a transport that can send bytes to a peer ID and +// deliver bytes back with the sender's identity attached; today that is a rage peer (see libs/x/rage), +// and it could as easily be direct RPC between processes that already know each other's addresses. +// What holds the guarantees up is the signature on every message body, not the transport underneath. +// +// It lives under libs/x because it is on its way somewhere else. crecore is meant to own DON-to-DON +// communication outright, so that a new process talks to another DON by asking crecore rather than +// by running any of this itself. When core no longer uses it, this moves into crecore and stops +// being importable. Until then it is here so core and crecore build against the same code, and +// nothing else should import it. +// +// Moved from chainlink's core/capabilities/remote (plus core/capabilities/transmission and +// core/capabilities/validation). Two things changed on the way: the dispatcher's configuration is a +// struct rather than an interface onto a node's TOML (see config.go), and peer types come from +// libs/x/rage rather than core/services/p2p/types. +package don2don diff --git a/libs/x/don2don/executable/client.go b/libs/x/don2don/executable/client.go new file mode 100644 index 000000000..e0dc43445 --- /dev/null +++ b/libs/x/don2don/executable/client.go @@ -0,0 +1,320 @@ +package executable + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + "github.com/smartcontractkit/capabilities/libs/x/don2don/executable/request" + "github.com/smartcontractkit/capabilities/libs/x/don2don/transmission" + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" +) + +// client is a shim for remote executable capabilities. +// It translates between capability API calls and network messages. +// Its responsibilities are: +// 1. Transmit capability requests to remote nodes according to a transmission schedule +// 2. Aggregate responses from remote nodes and return the aggregated response +// +// client communicates with corresponding server on remote nodes. +type client struct { + services.StateMachine + capabilityID string + capMethodName string + dispatcher types.Dispatcher + cfg atomic.Pointer[dynamicConfig] + lggr logger.Logger + + requestIDToCallerRequest map[string]*request.ClientRequest + mutex sync.Mutex + stopCh services.StopChan + wg sync.WaitGroup +} + +type dynamicConfig struct { + remoteCapabilityInfo commoncap.CapabilityInfo + localDONInfo commoncap.DON + requestTimeout time.Duration + // Has to be set only for V2 capabilities. V1 capabilities read transmission schedule from every request. + transmissionConfig *transmission.TransmissionConfig + // Has to be set only for V2 capabilities using OCR. + signers [][]byte + // minResponsesToAggregate overrides the default F+1 matching threshold for read consensus. + // 0 defaults to F+1. + minResponsesToAggregate uint32 +} + +type Client interface { + commoncap.ExecutableCapability + Receive(ctx context.Context, msg *types.MessageBody) + SetConfig(remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig, signers [][]byte, minResponsesToAggregate uint32) error +} + +var _ Client = &client{} +var _ types.Receiver = &client{} +var _ services.Service = &client{} + +const defaultExpiryCheckInterval = 30 * time.Second + +var ( + ErrRequestExpired = errors.New("request expired by executable client") + ErrContextDoneBeforeResponseQuorum = errors.New("context done before remote client received a quorum of responses") +) + +func NewClient(capabilityID string, capMethodName string, dispatcher types.Dispatcher, lggr logger.Logger) *client { + return &client{ + capabilityID: capabilityID, + capMethodName: capMethodName, + dispatcher: dispatcher, + lggr: logger.With(logger.Named(lggr, "ExecutableCapabilityClient"), "capabilityID", capabilityID, "capMethodName", capMethodName), + requestIDToCallerRequest: make(map[string]*request.ClientRequest), + stopCh: make(services.StopChan), + } +} + +// SetConfig sets the remote capability configuration dynamically +// TransmissionConfig has to be set only for V2 capabilities. V1 capabilities read transmission schedule from every request. +func (c *client) SetConfig(remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig, signers [][]byte, minResponsesToAggregate uint32) error { + if remoteCapabilityInfo.ID == "" || remoteCapabilityInfo.ID != c.capabilityID { + return fmt.Errorf("capability info provided does not match the client's capabilityID: %s != %s", remoteCapabilityInfo.ID, c.capabilityID) + } + if remoteCapabilityInfo.DON == nil { + return errors.New("remote capability info missing DON") + } + if len(localDonInfo.Members) == 0 { + return errors.New("empty localDonInfo provided") + } + if requestTimeout <= 0 { + return errors.New("requestTimeout must be positive") + } + + // always replace the whole dynamicConfig object to avoid inconsistent state + c.cfg.Store(&dynamicConfig{ + remoteCapabilityInfo: remoteCapabilityInfo, + localDONInfo: localDonInfo, + requestTimeout: requestTimeout, + transmissionConfig: transmissionConfig, + signers: signers, + minResponsesToAggregate: minResponsesToAggregate, + }) + c.lggr.Infow("SetConfig", "remoteDONName", remoteCapabilityInfo.DON.Name, "remoteDONID", remoteCapabilityInfo.DON.ID, "requestTimeout", requestTimeout, "transmissionConfig", transmissionConfig) + return nil +} + +func (c *client) Start(ctx context.Context) error { + return c.StartOnce(c.Name(), func() error { + cfg := c.cfg.Load() + + // Validate that all required fields are set before starting + if cfg == nil { + return errors.New("config not set - call SetConfig() before Start()") + } + if cfg.remoteCapabilityInfo.ID == "" { + return errors.New("remote capability info not set - call SetConfig() before Start()") + } + if len(cfg.localDONInfo.Members) == 0 { + return errors.New("local DON info not set - call SetConfig() before Start()") + } + if cfg.requestTimeout <= 0 { + return errors.New("request timeout not set - call SetConfig() before Start()") + } + if c.dispatcher == nil { + return errors.New("dispatcher set to nil, cannot start client") + } + + c.wg.Go(func() { + c.checkForExpiredRequests() + }) + c.wg.Go(func() { + c.checkDispatcherReady() + }) + + c.lggr.Info("ExecutableCapabilityClient started") + return nil + }) +} + +func (c *client) Close() error { + return c.StopOnce(c.Name(), func() error { + close(c.stopCh) + c.cancelAllRequests(errors.New("client closed")) + c.wg.Wait() + c.lggr.Info("ExecutableCapabilityClient closed") + return nil + }) +} + +func (c *client) checkDispatcherReady() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { + select { + case <-c.stopCh: + return + case <-ticker.C: + if err := c.dispatcher.Ready(); err != nil { + c.cancelAllRequests(fmt.Errorf("dispatcher not ready: %w", err)) + } + } + } +} + +func (c *client) checkForExpiredRequests() { + ticker := time.NewTicker(getClientTickerInterval(c.cfg.Load())) + defer ticker.Stop() + for { + select { + case <-c.stopCh: + return + case <-ticker.C: + ticker.Reset(getClientTickerInterval(c.cfg.Load())) + c.expireRequests() + } + } +} + +func getClientTickerInterval(cfg *dynamicConfig) time.Duration { + if cfg != nil && cfg.requestTimeout > 0 { + return cfg.requestTimeout + } + return defaultExpiryCheckInterval +} + +func (c *client) expireRequests() { + c.mutex.Lock() + defer c.mutex.Unlock() + + for messageID, req := range c.requestIDToCallerRequest { + if req.Expired() { + req.Cancel(ErrRequestExpired) + delete(c.requestIDToCallerRequest, messageID) + } + + if c.dispatcher.Ready() != nil { + c.cancelAllRequests(errors.New("dispatcher not ready")) + return + } + } +} + +func (c *client) cancelAllRequests(err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + for _, req := range c.requestIDToCallerRequest { + req.Cancel(err) + } +} + +func (c *client) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + cfg := c.cfg.Load() + if cfg == nil { + return commoncap.CapabilityInfo{}, errors.New("config not set - call SetConfig() before Info()") + } + return cfg.remoteCapabilityInfo, nil +} + +func (c *client) RegisterToWorkflow(ctx context.Context, registerRequest commoncap.RegisterToWorkflowRequest) error { + return nil +} + +func (c *client) UnregisterFromWorkflow(ctx context.Context, unregisterRequest commoncap.UnregisterFromWorkflowRequest) error { + return nil +} + +func (c *client) Execute(ctx context.Context, capReq commoncap.CapabilityRequest) (commoncap.CapabilityResponse, error) { + cfg := c.cfg.Load() + if cfg == nil { + return commoncap.CapabilityResponse{}, errors.New("config not set - call SetConfig() before Execute()") + } + + req, err := request.NewClientExecuteRequest(ctx, c.lggr, capReq, cfg.remoteCapabilityInfo, cfg.localDONInfo, c.dispatcher, + cfg.requestTimeout, cfg.transmissionConfig, c.capMethodName, cfg.signers, cfg.minResponsesToAggregate) + if err != nil { + return commoncap.CapabilityResponse{}, fmt.Errorf("failed to create client request: %w", err) + } + c.lggr.Debugw("created new client request", "requestID", req.ID()) + + if err = c.storeRequest(req); err != nil { + return commoncap.CapabilityResponse{}, fmt.Errorf("failed to store request: %w", err) + } + + var respResult []byte + var respErr error + select { + case resp := <-req.ResponseChan(): + respResult = resp.Result + respErr = resp.Err + case <-ctx.Done(): + // NOTE: ClientRequest will not block on sending to ResponseChan() because that channel is buffered (with size 1) + return commoncap.CapabilityResponse{}, errors.Join(ErrContextDoneBeforeResponseQuorum, ctx.Err()) + } + + if respErr != nil { + return commoncap.CapabilityResponse{}, fmt.Errorf("error executing request: %w", respErr) + } + + capabilityResponse, err := pb.UnmarshalCapabilityResponse(respResult) + if err != nil { + return commoncap.CapabilityResponse{}, fmt.Errorf("failed to unmarshal capability response: %w", err) + } + + if c.cfg.Load() != nil { + capabilityResponse.Metadata.CapDON_N = uint32(len(c.cfg.Load().localDONInfo.Members)) //nolint:gosec // G115 + } + + return capabilityResponse, nil +} + +func (c *client) storeRequest(req *request.ClientRequest) error { + c.mutex.Lock() + defer c.mutex.Unlock() + if _, ok := c.requestIDToCallerRequest[req.ID()]; ok { + return fmt.Errorf("request for ID %s already exists", req.ID()) + } + c.requestIDToCallerRequest[req.ID()] = req + return nil +} + +func (c *client) Receive(ctx context.Context, msg *types.MessageBody) { + c.mutex.Lock() + defer c.mutex.Unlock() + + messageID, err := GetMessageID(msg) + if err != nil { + c.lggr.Errorw("invalid message ID", "err", err, "id", don2don.SanitizeLogString(string(msg.MessageId))) + return + } + + c.lggr.Debugw("Remote client executable receiving message", "messageID", messageID) + + req := c.requestIDToCallerRequest[messageID] + if req == nil { + c.lggr.Warnw("received response for unknown message ID ", "messageID", messageID) + return + } + + if err := req.OnMessage(ctx, msg); err != nil { + c.lggr.Errorw("failed to add response to request", "messageID", messageID, "err", err) + } +} + +func (c *client) Ready() error { + return nil +} + +func (c *client) HealthReport() map[string]error { + return nil +} + +func (c *client) Name() string { + return c.lggr.Name() +} diff --git a/libs/x/don2don/executable/client_test.go b/libs/x/don2don/executable/client_test.go new file mode 100644 index 000000000..ed971b237 --- /dev/null +++ b/libs/x/don2don/executable/client_test.go @@ -0,0 +1,635 @@ +package executable_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/executable" + "github.com/smartcontractkit/capabilities/libs/x/don2don/transmission" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/internal/synctest" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +const ( + stepReferenceID1 = "step1" + workflowID1 = "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0" + workflowID2 = "25c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce1" + workflowExecutionID1 = "95ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0abbadeed" + workflowExecutionID2 = "85ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0abbadeee" + workflowOwnerID = "0xAA" +) + +func Test_Client_DonTopologies(t *testing.T) { + t.Parallel() + + tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/CAPPL-322") + + ctx := t.Context() + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + responseTest := func(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + if assert.NoError(t, responseError) { + mp, err := response.Value.Unwrap() + if assert.NoError(t, err) { + assert.Equal(t, "aValue1", mp.(map[string]any)["response"].(string)) + } + } + } + + capability := &TestCapability{} + + responseTimeOut := 10 * time.Minute + + var methods []func(caller commoncap.ExecutableCapability) + + methods = make([]func(caller commoncap.ExecutableCapability), 0, 1) + methods = append(methods, func(caller commoncap.ExecutableCapability) { + executeInputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + if assert.NoError(t, err) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, responseTest, t) + } + }) + + for _, method := range methods { + testClient(t, 1, responseTimeOut, 1, 0, + capability, method) + + testClient(t, 10, responseTimeOut, 1, 0, + capability, method) + + testClient(t, 1, responseTimeOut, 10, 3, + capability, method) + + testClient(t, 10, responseTimeOut, 10, 3, + capability, method) + + testClient(t, 10, responseTimeOut, 10, 9, + capability, method) + } +} + +func Test_Client_TransmissionSchedules(t *testing.T) { + t.Parallel() + + tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/DX-104") + ctx := t.Context() + + responseTest := func(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + if assert.NoError(t, responseError) { + mp, err := response.Value.Unwrap() + if assert.NoError(t, err) { + assert.Equal(t, "aValue1", mp.(map[string]any)["response"].(string)) + } + } + } + + capability := &TestCapability{} + + responseTimeOut := 10 * time.Minute + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testClient(t, 1, responseTimeOut, 1, 0, + capability, func(caller commoncap.ExecutableCapability) { + executeInputs, err2 := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + if assert.NoError(t, err2) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, responseTest, t) + } + }, + ) + testClient(t, 10, responseTimeOut, 10, 3, + capability, func(caller commoncap.ExecutableCapability) { + executeInputs, err2 := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + if assert.NoError(t, err2) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, responseTest, t) + } + }, + ) + + transmissionSchedule, err = values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testClient(t, 1, responseTimeOut, 1, 0, + capability, func(caller commoncap.ExecutableCapability) { + executeInputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + if assert.NoError(t, err) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, responseTest, t) + } + }) + testClient(t, 10, responseTimeOut, 10, 3, + capability, func(caller commoncap.ExecutableCapability) { + executeInputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + if assert.NoError(t, err) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, responseTest, t) + } + }) +} + +func Test_Client_ResponseAggregationGrace(t *testing.T) { + t.Parallel() + const ( + requestTimeout = 500 * time.Millisecond + capabilityExecuteDelay = 2 * time.Second + ) + + for _, tc := range []struct { + name string + numWorkflowPeers int + numCapabilityPeers int + capabilityDonF uint8 + responseTest func(t *testing.T, response commoncap.CapabilityResponse, responseError error) + }{ + // Client cancels at requestTimeout + responseAggregationGrace (~10.5s). + // Ten workflow peers force clientTestServer to wait for the full workflow DON + // and block the broker during Execute; quorum (F+1=4) is theoretically + // reachable but responses arrive after the grace window. + { + name: "TimesOutWhenMockBlockingExceedsGrace", + numWorkflowPeers: 10, + numCapabilityPeers: 10, + capabilityDonF: 3, + responseTest: assertClientRequestExpired, + }, + // capabilityExecuteDelay exceeds requestTimeout but fits within + // requestTimeout + responseAggregationGrace. + // Use a single workflow peer so the mock cap server executes as soon as it + // receives one request; with multiple workflow peers the mock waits for all + // peers and blocks the test broker while executing, which exceeds the grace window. + { + name: "WaitsForResponsesWithinGrace", + numWorkflowPeers: 1, + numCapabilityPeers: 4, + capabilityDonF: 3, + responseTest: assertClientResponseWithDelay, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + ctx := t.Context() + + capability := &TestSlowExecutionCapability{ + workflowIDToPause: map[string]time.Duration{ + workflowID1: capabilityExecuteDelay, + }, + } + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + executeInputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + require.NoError(t, err) + + testClient(t, tc.numWorkflowPeers, requestTimeout, tc.numCapabilityPeers, tc.capabilityDonF, + capability, + func(caller commoncap.ExecutableCapability) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, tc.responseTest, t) + }, + ) + }) + }) + } +} + +func assertClientRequestExpired(t *testing.T, _ commoncap.CapabilityResponse, responseError error) { + assert.ErrorIs(t, responseError, executable.ErrRequestExpired) +} + +func assertClientResponseWithDelay(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + if assert.NoError(t, responseError) { + mp, err := response.Value.Unwrap() + if assert.NoError(t, err) { + assert.Equal(t, (2 * time.Second).String(), mp.(map[string]any)["response"].(string)) + } + } +} + +func Test_Client_ConsensusFailedIfInsufficientCapabilityPeerResponses(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + responseTest := func(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + var capErr caperrors.Error + require.ErrorAs(t, responseError, &capErr) + require.Equal(t, caperrors.ConsensusFailed, capErr.Code()) + require.Contains(t, capErr.Error(), "[100]ConsensusFailed: response quorum unreachable: not enough matching capability responses: received 1/10 peer responses with 1 unique payloads; best match count 1, need 12 (9 responses pending)") + } + + capability := &TestCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + // F+1 exceeds peer count; first divergent response makes quorum unreachable. + + testClient(t, 10, 1*time.Second, 10, 11, + capability, + func(caller commoncap.ExecutableCapability) { + executeInputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + if assert.NoError(t, err) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, responseTest, t) + } + }) +} + +func Test_Client_ContextCanceledBeforeQuorumReached(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + + responseTest := func(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + assert.ErrorIs(t, responseError, executable.ErrContextDoneBeforeResponseQuorum) + } + + capability := &TestCapability{} + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "20s", + }) + require.NoError(t, err) + + cancel() + testClient(t, 2, 20*time.Second, 2, 2, + capability, + func(caller commoncap.ExecutableCapability) { + executeInputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + if assert.NoError(t, err) { + executeMethod(ctx, caller, transmissionSchedule, executeInputs, responseTest, t) + } + }) +} + +func testClient(t *testing.T, numWorkflowPeers int, workflowNodeResponseTimeout time.Duration, numCapabilityPeers int, capabilityDonF uint8, underlying commoncap.ExecutableCapability, method func(caller commoncap.ExecutableCapability)) { + lggr := logger.Test(t) + + capabilityPeers := make([]rage.PeerID, numCapabilityPeers) + for i := range numCapabilityPeers { + capabilityPeers[i] = NewP2PPeerID(t) + } + + capDonInfo := commoncap.DON{ + ID: 1, + Members: capabilityPeers, + F: capabilityDonF, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1.0.0", + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Executable Capability", + DON: &capDonInfo, + } + + workflowPeers := make([]rage.PeerID, numWorkflowPeers) + for i := range numWorkflowPeers { + workflowPeers[i] = NewP2PPeerID(t) + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: 2, + } + + broker := newTestAsyncMessageBroker(t, 100) + + receivers := make([]don2dontypes.Receiver, numCapabilityPeers) + for i := range numCapabilityPeers { + capabilityDispatcher := broker.NewDispatcherForNode(capabilityPeers[i]) + receiver := newTestServer(capabilityPeers[i], capabilityDispatcher, workflowDonInfo, underlying) + broker.RegisterReceiverNode(capabilityPeers[i], receiver) + receivers[i] = receiver + } + + callers := make([]commoncap.ExecutableCapability, numWorkflowPeers) + + for i := range numWorkflowPeers { + workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i]) + caller := executable.NewClient(capInfo.ID, "", workflowPeerDispatcher, lggr) + err := caller.SetConfig(capInfo, workflowDonInfo, workflowNodeResponseTimeout, nil, nil, 0) + require.NoError(t, err) + servicetest.Run(t, caller) + broker.RegisterReceiverNode(workflowPeers[i], caller) + callers[i] = caller + } + + servicetest.Run(t, broker) + + wg := &sync.WaitGroup{} + wg.Add(len(callers)) + + // Fire off all the requests + for _, caller := range callers { + go func(caller commoncap.ExecutableCapability) { + defer wg.Done() + method(caller) + }(caller) + } + + wg.Wait() +} + +func executeMethod(ctx context.Context, caller commoncap.ExecutableCapability, transmissionSchedule *values.Map, + executeInputs *values.Map, responseTest func(t *testing.T, responseCh commoncap.CapabilityResponse, responseError error), t *testing.T) { + responseCh, err := caller.Execute(ctx, + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + WorkflowOwner: workflowOwnerID, + }, + Config: transmissionSchedule, + Inputs: executeInputs, + }) + + responseTest(t, responseCh, err) +} + +// Simple client that only responds once it has received a message from each workflow peer +type clientTestServer struct { + peerID rage.PeerID + dispatcher don2dontypes.Dispatcher + workflowDonInfo commoncap.DON + messageIDToSenders map[string]map[rage.PeerID]bool + + executableCapability commoncap.ExecutableCapability + + mux sync.Mutex +} + +func newTestServer(peerID rage.PeerID, dispatcher don2dontypes.Dispatcher, workflowDonInfo commoncap.DON, + executableCapability commoncap.ExecutableCapability) *clientTestServer { + return &clientTestServer{ + dispatcher: dispatcher, + workflowDonInfo: workflowDonInfo, + peerID: peerID, + messageIDToSenders: make(map[string]map[rage.PeerID]bool), + executableCapability: executableCapability, + } +} + +func (t *clientTestServer) Receive(ctx context.Context, msg *don2dontypes.MessageBody) { + t.mux.Lock() + defer t.mux.Unlock() + + sender := toPeerID(msg.Sender) + messageID, err := executable.GetMessageID(msg) + if err != nil { + panic(err) + } + + if t.messageIDToSenders[messageID] == nil { + t.messageIDToSenders[messageID] = make(map[rage.PeerID]bool) + } + + sendersOfMessageID := t.messageIDToSenders[messageID] + if sendersOfMessageID[sender] { + panic("received duplicate message") + } + + sendersOfMessageID[sender] = true + + if len(t.messageIDToSenders[messageID]) == len(t.workflowDonInfo.Members) { + switch msg.Method { + case don2dontypes.MethodExecute: + capabilityRequest, err := pb.UnmarshalCapabilityRequest(msg.Payload) + if err != nil { + panic(err) + } + resp, responseErr := t.executableCapability.Execute(ctx, capabilityRequest) + payload, marshalErr := pb.MarshalCapabilityResponse(resp) + t.sendResponse(messageID, responseErr, payload, marshalErr) + default: + panic("unknown method") + } + } +} + +func (t *clientTestServer) sendResponse(messageID string, responseErr error, + payload []byte, marshalErr error) { + for receiver := range t.messageIDToSenders[messageID] { + var responseMsg = &don2dontypes.MessageBody{ + CapabilityId: "cap_id@1.0.0", + CapabilityDonId: 1, + CallerDonId: t.workflowDonInfo.ID, + Method: don2dontypes.MethodExecute, + MessageId: []byte(messageID), + Sender: t.peerID[:], + Receiver: receiver[:], + } + + if responseErr != nil { + responseMsg.Error = don2dontypes.Error_INTERNAL_ERROR + } else { + if marshalErr != nil { + panic(marshalErr) + } + responseMsg.Payload = payload + } + + err := t.dispatcher.Send(receiver, responseMsg) + if err != nil { + panic(err) + } + } +} + +type clientSetConfigTestFixture struct { + Client interface { + SetConfig(commoncap.CapabilityInfo, commoncap.DON, time.Duration, *transmission.TransmissionConfig, [][]byte, uint32) error + Info(context.Context) (commoncap.CapabilityInfo, error) + Start(context.Context) error + Close() error + } + ValidCapInfo commoncap.CapabilityInfo + ValidDonInfo commoncap.DON + ValidTimeout time.Duration + CapabilityID string +} + +func newClientSetConfigTestFixture(t *testing.T) clientSetConfigTestFixture { + t.Helper() + + capabilityID := "test_capability@1.0.0" + broker := newTestAsyncMessageBroker(t, 100) + peerID := NewP2PPeerID(t) + dispatcher := broker.NewDispatcherForNode(peerID) + client := executable.NewClient(capabilityID, "execute", dispatcher, logger.Test(t)) + + validDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{NewP2PPeerID(t)}, + F: 0, + } + + validCapInfo := commoncap.CapabilityInfo{ + ID: capabilityID, + CapabilityType: commoncap.CapabilityTypeAction, + Description: "Test capability", + DON: &validDonInfo, + } + + return clientSetConfigTestFixture{ + Client: client, + ValidCapInfo: validCapInfo, + ValidDonInfo: validDonInfo, + ValidTimeout: 30 * time.Second, + CapabilityID: capabilityID, + } +} + +func TestClient_SetConfig(t *testing.T) { + t.Parallel() + + t.Run("successful config set", func(t *testing.T) { + t.Parallel() + + fixture := newClientSetConfigTestFixture(t) + + transmissionConfig := &transmission.TransmissionConfig{ + Schedule: transmission.Schedule_OneAtATime, + DeltaStage: 10 * time.Millisecond, + } + + err := fixture.Client.SetConfig(fixture.ValidCapInfo, fixture.ValidDonInfo, fixture.ValidTimeout, transmissionConfig, nil, 0) + require.NoError(t, err) + + info, err := fixture.Client.Info(t.Context()) + require.NoError(t, err) + assert.Equal(t, fixture.ValidCapInfo.ID, info.ID) + }) + + t.Run("mismatched capability ID", func(t *testing.T) { + t.Parallel() + + fixture := newClientSetConfigTestFixture(t) + + invalidCapInfo := commoncap.CapabilityInfo{ + ID: "different_capability@1.0.0", + CapabilityType: commoncap.CapabilityTypeAction, + } + + err := fixture.Client.SetConfig(invalidCapInfo, fixture.ValidDonInfo, fixture.ValidTimeout, nil, nil, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "capability info provided does not match the client's capabilityID") + assert.Contains(t, err.Error(), "different_capability@1.0.0 != test_capability@1.0.0") + }) + + t.Run("empty DON members", func(t *testing.T) { + t.Parallel() + + fixture := newClientSetConfigTestFixture(t) + + invalidDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{}, + F: 0, + } + + err := fixture.Client.SetConfig(fixture.ValidCapInfo, invalidDonInfo, fixture.ValidTimeout, nil, nil, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "empty localDonInfo provided") + }) + + t.Run("successful config update", func(t *testing.T) { + t.Parallel() + + fixture := newClientSetConfigTestFixture(t) + + initialTimeout := 10 * time.Second + err := fixture.Client.SetConfig(fixture.ValidCapInfo, fixture.ValidDonInfo, initialTimeout, nil, nil, 0) + require.NoError(t, err) + + newTimeout := 60 * time.Second + newDonInfo := commoncap.DON{ + ID: 2, + Members: []rage.PeerID{NewP2PPeerID(t), NewP2PPeerID(t)}, + F: 1, + } + + err = fixture.Client.SetConfig(fixture.ValidCapInfo, newDonInfo, newTimeout, nil, nil, 0) + require.NoError(t, err) + + info, err := fixture.Client.Info(t.Context()) + require.NoError(t, err) + assert.Equal(t, fixture.ValidCapInfo.ID, info.ID) + }) +} + +func TestClient_SetConfig_StartClose(t *testing.T) { + t.Parallel() + + t.Run("start fails without config", func(t *testing.T) { + t.Parallel() + + fixture := newClientSetConfigTestFixture(t) + + err := fixture.Client.Start(t.Context()) + require.Error(t, err) + assert.Contains(t, err.Error(), "config not set - call SetConfig() before Start()") + }) + + t.Run("start succeeds after config set", func(t *testing.T) { + t.Parallel() + + fixture := newClientSetConfigTestFixture(t) + ctx := t.Context() + + require.NoError(t, fixture.Client.SetConfig(fixture.ValidCapInfo, fixture.ValidDonInfo, fixture.ValidTimeout, nil, nil, 0)) + require.NoError(t, fixture.Client.Start(ctx)) + require.NoError(t, fixture.Client.Close()) + }) + + t.Run("config can be updated after start", func(t *testing.T) { + t.Parallel() + + fixture := newClientSetConfigTestFixture(t) + ctx := t.Context() + + require.NoError(t, fixture.Client.SetConfig(fixture.ValidCapInfo, fixture.ValidDonInfo, fixture.ValidTimeout, nil, nil, 0)) + require.NoError(t, fixture.Client.Start(ctx)) + + newCapInfo := fixture.ValidCapInfo + newCapInfo.Description = "new description" + require.NoError(t, fixture.Client.SetConfig(newCapInfo, fixture.ValidDonInfo, fixture.ValidTimeout, nil, nil, 0)) + + info, err := fixture.Client.Info(ctx) + require.NoError(t, err) + assert.Equal(t, newCapInfo.Description, info.Description) + + require.NoError(t, fixture.Client.Close()) + }) +} diff --git a/libs/x/don2don/executable/endtoend_test.go b/libs/x/don2don/executable/endtoend_test.go new file mode 100644 index 000000000..335363c1a --- /dev/null +++ b/libs/x/don2don/executable/endtoend_test.go @@ -0,0 +1,561 @@ +package executable_test + +import ( + "context" + "crypto/rand" + "errors" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/executable" + "github.com/smartcontractkit/capabilities/libs/x/don2don/transmission" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/internal/synctest" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +func Test_RemoteExecutableCapability_ExecutionNotBlockedBySlowCapabilityExecution(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + schedule string + }{ + {"AllAtOnce", transmission.Schedule_AllAtOnce}, + {"OneAtATime", transmission.Schedule_OneAtATime}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + ctx := t.Context() + + capability := &TestSlowExecutionCapability{ + workflowIDToPause: map[string]time.Duration{ + workflowID1: synctest.SlowDelay(), + workflowID2: synctest.FastDelay(), + }, + } + + const numWorkflowPeers = 10 + // Slow capability delay is 1m; timeout must exceed that for phase-1 assertions after synctest.Wait. + requestTimeout := 2 * time.Minute + + harness := setupRemoteExecutableHarness(t, capability, numWorkflowPeers, 9, requestTimeout, 10, 9, requestTimeout) + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": tc.schedule, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + executeInputs, err := values.NewMap(map[string]any{ + "executeValue1": "aValue1", + }) + require.NoError(t, err) + + var wgSlow sync.WaitGroup + wgSlow.Add(len(harness.workflowNodes)) + for _, caller := range harness.workflowNodes { + go func(caller commoncap.ExecutableCapability) { + defer wgSlow.Done() + executeCapability(ctx, t, caller, transmissionSchedule, executeInputs, func(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + if assert.NoError(t, responseError) { + mp, err := response.Value.Unwrap() + if assert.NoError(t, err) { + assert.Equal(t, synctest.SlowDelay().String(), mp.(map[string]any)["response"].(string)) + } + } + }, workflowID1, workflowExecutionID1) + }(caller) + } + + var wgFast sync.WaitGroup + wgFast.Add(len(harness.workflowNodes)) + for _, caller := range harness.workflowNodes { + go func(caller commoncap.ExecutableCapability) { + defer wgFast.Done() + executeCapability(ctx, t, caller, transmissionSchedule, executeInputs, func(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + if assert.NoError(t, responseError) { + mp, err := response.Value.Unwrap() + if assert.NoError(t, err) { + assert.Equal(t, synctest.FastDelay().String(), mp.(map[string]any)["response"].(string)) + } + } + }, workflowID2, workflowExecutionID2) + }(caller) + } + + wgFast.Wait() + synctest.Wait() + wgSlow.Wait() + }) + }) + } +} + +func Test_RemoteExecutableCapability_TransmissionSchedules(t *testing.T) { + t.Parallel() + + tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/DX-108") + ctx := t.Context() + + responseTest := func(t *testing.T, response commoncap.CapabilityResponse, responseError error) { + if assert.NoError(t, responseError) { + mp, err := response.Value.Unwrap() + if assert.NoError(t, err) { + assert.Equal(t, "aValue1", mp.(map[string]any)["response"].(string)) + } + } + } + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + timeOut := 10 * time.Minute + + capability := &TestCapability{} + + executeInputs, err := values.NewMap(map[string]any{ + "executeValue1": "aValue1", + }) + require.NoError(t, err) + + method := func(ctx context.Context, caller commoncap.ExecutableCapability) { + executeCapability(ctx, t, caller, transmissionSchedule, executeInputs, responseTest, workflowID1, workflowExecutionID1) + } + testRemoteExecutableCapability(ctx, t, capability, 10, 9, timeOut, 10, 9, timeOut, method, true) + + transmissionSchedule, err = values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + method = func(ctx context.Context, caller commoncap.ExecutableCapability) { + executeCapability(ctx, t, caller, transmissionSchedule, executeInputs, responseTest, workflowID1, workflowExecutionID1) + } + + testRemoteExecutableCapability(ctx, t, capability, 10, 9, timeOut, 10, 9, timeOut, method, true) +} + +func Test_RemoteExecutionCapability_CapabilityError(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + capability := &TestErrorCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + executeInputs, err := values.NewMap(map[string]any{ + "executeValue1": "aValue1", + }) + require.NoError(t, err) + + var methods []func(ctx context.Context, caller commoncap.ExecutableCapability) + + methods = make([]func(ctx context.Context, caller commoncap.ExecutableCapability), 0, 1) + methods = append(methods, func(ctx context.Context, caller commoncap.ExecutableCapability) { + executeCapability(ctx, t, caller, transmissionSchedule, executeInputs, func(t *testing.T, responseCh commoncap.CapabilityResponse, responseError error) { + assert.ErrorContains(t, responseError, "failed to execute capability") + }, workflowID1, workflowExecutionID1) + }) + + for _, method := range methods { + testRemoteExecutableCapability(ctx, t, capability, 10, 9, 10*time.Minute, 10, 9, 10*time.Minute, method, true) + } +} + +func Test_RemoteExecutableCapability_RandomCapabilityError(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + capability := &TestRandomErrorCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + executeInputs, err := values.NewMap(map[string]any{ + "executeValue1": "aValue1", + }) + require.NoError(t, err) + + var methods []func(ctx context.Context, caller commoncap.ExecutableCapability) + + methods = make([]func(ctx context.Context, caller commoncap.ExecutableCapability), 0, 1) + methods = append(methods, func(ctx context.Context, caller commoncap.ExecutableCapability) { + executeCapability(ctx, t, caller, transmissionSchedule, executeInputs, func(t *testing.T, responseCh commoncap.CapabilityResponse, responseError error) { + assert.ErrorContains(t, responseError, "failed to execute capability") + }, workflowID1, workflowExecutionID1) + }) + + for _, method := range methods { + testRemoteExecutableCapability(ctx, t, capability, 10, 9, 1*time.Second, 10, 9, 10*time.Minute, + method, true) + } +} + +type remoteExecutableHarness struct { + workflowNodes []commoncap.ExecutableCapability +} + +func setupRemoteExecutableHarness(t *testing.T, underlying commoncap.ExecutableCapability, numWorkflowPeers int, workflowDonF uint8, workflowNodeTimeout time.Duration, + numCapabilityPeers int, capabilityDonF uint8, capabilityNodeResponseTimeout time.Duration) remoteExecutableHarness { + t.Helper() + + lggr := logger.Test(t) + + capabilityPeers := make([]rage.PeerID, numCapabilityPeers) + for i := range numCapabilityPeers { + capabilityPeerID := rage.PeerID{} + require.NoError(t, capabilityPeerID.UnmarshalText([]byte(NewPeerID()))) + capabilityPeers[i] = capabilityPeerID + } + + capDonInfo := commoncap.DON{ + ID: 2, + Members: capabilityPeers, + F: capabilityDonF, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1.0.0", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Remote Target", + DON: &capDonInfo, + } + + workflowPeers := make([]rage.PeerID, numWorkflowPeers) + for i := range numWorkflowPeers { + workflowPeerID := rage.PeerID{} + require.NoError(t, workflowPeerID.UnmarshalText([]byte(NewPeerID()))) + workflowPeers[i] = workflowPeerID + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: 1, + F: workflowDonF, + } + + broker := newTestAsyncMessageBroker(t, 1000) + + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + for i := range numCapabilityPeers { + capabilityPeer := capabilityPeers[i] + capabilityDispatcher := broker.NewDispatcherForNode(capabilityPeer) + capabilityNode := executable.NewServer(capInfo.ID, "", capabilityPeer, capabilityDispatcher, limits.NewGateLimiter(false), lggr) + cfg := &commoncap.RemoteExecutableConfig{ + RequestHashExcludedAttributes: []string{}, + RequestTimeout: capabilityNodeResponseTimeout, + ServerMaxParallelRequests: 10, + } + require.NoError(t, capabilityNode.SetConfig(cfg, underlying, capInfo, capDonInfo, workflowDONs, nil)) + servicetest.Run(t, capabilityNode) + broker.RegisterReceiverNode(capabilityPeer, capabilityNode) + } + + workflowNodes := make([]commoncap.ExecutableCapability, numWorkflowPeers) + for i := range numWorkflowPeers { + workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i]) + workflowNode := executable.NewClient(capInfo.ID, "", workflowPeerDispatcher, lggr) + err := workflowNode.SetConfig(capInfo, workflowDonInfo, workflowNodeTimeout, nil, nil, 0) + require.NoError(t, err) + servicetest.Run(t, workflowNode) + broker.RegisterReceiverNode(workflowPeers[i], workflowNode) + workflowNodes[i] = workflowNode + } + + servicetest.Run(t, broker) + + return remoteExecutableHarness{workflowNodes: workflowNodes} +} + +func testRemoteExecutableCapability(ctx context.Context, t *testing.T, underlying commoncap.ExecutableCapability, numWorkflowPeers int, workflowDonF uint8, workflowNodeTimeout time.Duration, + numCapabilityPeers int, capabilityDonF uint8, capabilityNodeResponseTimeout time.Duration, + method func(ctx context.Context, caller commoncap.ExecutableCapability), waitForExecuteCalls bool) { + harness := setupRemoteExecutableHarness(t, underlying, numWorkflowPeers, workflowDonF, workflowNodeTimeout, numCapabilityPeers, capabilityDonF, capabilityNodeResponseTimeout) + + wg := &sync.WaitGroup{} + wg.Add(len(harness.workflowNodes)) + + for _, caller := range harness.workflowNodes { + go func(caller commoncap.ExecutableCapability) { + defer wg.Done() + method(ctx, caller) + }(caller) + } + if waitForExecuteCalls { + wg.Wait() + } +} + +type testAsyncMessageBroker struct { + services.Service + eng *services.Engine + t *testing.T + + nodes map[rage.PeerID]don2dontypes.Receiver + + sendCh chan *don2dontypes.MessageBody +} + +func newTestAsyncMessageBroker(t *testing.T, sendChBufferSize int) *testAsyncMessageBroker { + b := &testAsyncMessageBroker{ + t: t, + nodes: make(map[rage.PeerID]don2dontypes.Receiver), + sendCh: make(chan *don2dontypes.MessageBody, sendChBufferSize), + } + b.Service, b.eng = services.Config{ + Name: "testAsyncMessageBroker", + Start: b.start, + }.NewServiceEngine(logger.Test(t)) + return b +} + +func (a *testAsyncMessageBroker) start(ctx context.Context) error { + a.eng.Go(func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case msg := <-a.sendCh: + receiverID := toPeerID(msg.Receiver) + + receiver, ok := a.nodes[receiverID] + if !ok { + panic("server not found for peer id") + } + + receiver.Receive(a.t.Context(), msg) + } + } + }) + return nil +} + +func (a *testAsyncMessageBroker) NewDispatcherForNode(nodePeerID rage.PeerID) don2dontypes.Dispatcher { + return &nodeDispatcher{ + callerPeerID: nodePeerID, + broker: a, + } +} + +func (a *testAsyncMessageBroker) RegisterReceiverNode(nodePeerID rage.PeerID, node don2dontypes.Receiver) { + if _, ok := a.nodes[nodePeerID]; ok { + panic("node already registered") + } + + a.nodes[nodePeerID] = node +} + +func (a *testAsyncMessageBroker) Send(msg *don2dontypes.MessageBody) { + a.sendCh <- msg +} + +func toPeerID(id []byte) rage.PeerID { + return [32]byte(id) +} + +type broker interface { + Send(msg *don2dontypes.MessageBody) +} + +type nodeDispatcher struct { + callerPeerID rage.PeerID + broker broker +} + +func (t *nodeDispatcher) Name() string { + return "nodeDispatcher" +} + +func (t *nodeDispatcher) Start(ctx context.Context) error { + return nil +} + +func (t *nodeDispatcher) Close() error { + return nil +} + +func (t *nodeDispatcher) Ready() error { + return nil +} + +func (t *nodeDispatcher) HealthReport() map[string]error { + return nil +} + +func (t *nodeDispatcher) Send(peerID rage.PeerID, msgBody *don2dontypes.MessageBody) error { + msgBody.Version = 1 + msgBody.Sender = t.callerPeerID[:] + msgBody.Receiver = peerID[:] + msgBody.Timestamp = time.Now().UnixMilli() + t.broker.Send(msgBody) + return nil +} + +func (t *nodeDispatcher) SetReceiver(capabilityID string, donID uint32, receiver don2dontypes.Receiver) error { + return nil +} +func (t *nodeDispatcher) RemoveReceiver(capabilityID string, donID uint32) {} + +func (t *nodeDispatcher) SetReceiverForMethod(capabilityID string, donID uint32, methodName string, receiver don2dontypes.Receiver) error { + return nil +} +func (t *nodeDispatcher) RemoveReceiverForMethod(capabilityID string, donID uint32, methodName string) { +} + +type abstractTestCapability struct { +} + +func (t abstractTestCapability) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + return commoncap.CapabilityInfo{}, nil +} + +func (t abstractTestCapability) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { + return nil +} + +func (t abstractTestCapability) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { + return nil +} + +type TestCapability struct { + abstractTestCapability +} + +func (t TestCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (commoncap.CapabilityResponse, error) { + value := request.Inputs.Underlying["executeValue1"] + response, err := values.NewMap(map[string]any{"response": value}) + if err != nil { + return commoncap.CapabilityResponse{}, err + } + return commoncap.CapabilityResponse{ + Value: response, + }, nil +} + +type TestSlowExecutionCapability struct { + abstractTestCapability + workflowIDToPause map[string]time.Duration +} + +func (t *TestSlowExecutionCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (commoncap.CapabilityResponse, error) { + var delay time.Duration + + delay, ok := t.workflowIDToPause[request.Metadata.WorkflowID] + if !ok { + panic("workflowID not found") + } + + select { + case <-time.After(delay): + break + case <-ctx.Done(): + return commoncap.CapabilityResponse{}, nil + } + + response, err := values.NewMap(map[string]any{"response": delay.String()}) + if err != nil { + return commoncap.CapabilityResponse{}, err + } + return commoncap.CapabilityResponse{ + Value: response, + }, nil +} + +type TestErrorCapability struct { + abstractTestCapability +} + +func (t TestErrorCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (commoncap.CapabilityResponse, error) { + return commoncap.CapabilityResponse{}, errors.New("an error") +} + +func (t TestErrorCapability) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { + return errors.New("an error") +} + +func (t TestErrorCapability) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { + return errors.New("an error") +} + +type TestRandomErrorCapability struct { + abstractTestCapability +} + +func (t TestRandomErrorCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (commoncap.CapabilityResponse, error) { + return commoncap.CapabilityResponse{}, errors.New(uuid.New().String()) +} + +func (t TestRandomErrorCapability) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { + return errors.New(uuid.New().String()) +} + +func (t TestRandomErrorCapability) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { + return errors.New(uuid.New().String()) +} + +func NewP2PPeerID(t *testing.T) rage.PeerID { + id := rage.PeerID{} + require.NoError(t, id.UnmarshalText([]byte(NewPeerID()))) + return id +} + +func NewPeerID() string { + var privKey [32]byte + _, err := rand.Read(privKey[:]) + if err != nil { + panic(err) + } + + peerID := append(libp2pMagic(), privKey[:]...) + + return base58.Encode(peerID) +} + +func libp2pMagic() []byte { + return []byte{0x00, 0x24, 0x08, 0x01, 0x12, 0x20} +} + +func executeCapability(ctx context.Context, t *testing.T, caller commoncap.ExecutableCapability, transmissionSchedule *values.Map, executeInputs *values.Map, responseTest func(t *testing.T, response commoncap.CapabilityResponse, responseError error), + workflowID, workflowExecutionID string) { + response, err := caller.Execute(ctx, + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID, + WorkflowExecutionID: workflowExecutionID, + }, + Config: transmissionSchedule, + Inputs: executeInputs, + }) + + responseTest(t, response, err) +} diff --git a/libs/x/don2don/executable/hasher.go b/libs/x/don2don/executable/hasher.go new file mode 100644 index 000000000..ba7c40dca --- /dev/null +++ b/libs/x/don2don/executable/hasher.go @@ -0,0 +1,211 @@ +package executable + +import ( + "crypto/sha256" + "errors" + "fmt" + "strings" + "time" + + "google.golang.org/protobuf/types/known/anypb" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + aptoscappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/aptos" + evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" + solcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/solana" + stellarcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/stellar" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" +) + +// V1 Capabilities only need a hasher for the ChainWrite Target. +// This hasher excludes signatures from the Inputs map when hashing the request. +type v1Hasher struct { + requestHashExcludedAttributes []string +} + +func (r *v1Hasher) Hash(msg *types.MessageBody) ([32]byte, error) { + req, err := pb.UnmarshalCapabilityRequest(msg.Payload) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal capability request: %w", err) + } + + // An attribute called StepDependency is used to define a data dependency between steps, + // and not to provide input values; we should therefore disregard it when hashing the request + if len(r.requestHashExcludedAttributes) == 0 { + r.requestHashExcludedAttributes = []string{"StepDependency"} + } + + for _, path := range r.requestHashExcludedAttributes { + if req.Inputs != nil { + req.Inputs.DeleteAtPath(path) + } + } + + reqBytes, err := pb.MarshalCapabilityRequest(req) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal capability request: %w", err) + } + hash := sha256.Sum256(reqBytes) + return hash, nil +} + +func NewV1Hasher(requestHashExcludedAttributes []string) types.MessageHasher { + return &v1Hasher{ + requestHashExcludedAttributes: requestHashExcludedAttributes, + } +} + +// V2 Capabilities (Executables) default to a simple hasher that hashes the entire payload. +// WriteReport methods use a hasher that excludes signatures from the WriteReportRequest. +// Additional hashers can be added here as needed. +type simpleHasher struct { +} + +func (r *simpleHasher) Hash(msg *types.MessageBody) ([32]byte, error) { + req, err := pb.UnmarshalCapabilityRequest(msg.Payload) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal capability request: %w", err) + } + + // Exclude per-node-divergent metadata fields to ensure identical requests + // with different values produce the same hash + req.Metadata.SpendLimits = nil + req.Metadata.ExecutionTimestamp = time.Time{} + + reqBytes, err := pb.MarshalCapabilityRequest(req) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal capability request: %w", err) + } + hash := sha256.Sum256(reqBytes) + return hash, nil +} + +func NewSimpleHasher() types.MessageHasher { + return &simpleHasher{} +} + +type writeReportExcludeSignaturesHasher struct { +} + +func (r *writeReportExcludeSignaturesHasher) Hash(msg *types.MessageBody) ([32]byte, error) { + req, err := pb.UnmarshalCapabilityRequest(msg.Payload) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal capability request: %w", err) + } + if req.Payload == nil { + return [32]byte{}, errors.New("capability request payload is nil") + } + + // Exclude per-node-divergent metadata fields to ensure identical requests + // with different values produce the same hash + req.Metadata.SpendLimits = nil + req.Metadata.ExecutionTimestamp = time.Time{} + family, familyErr := getWriteReportFamily(msg) + if familyErr != nil { + return [32]byte{}, familyErr + } + + var payload *anypb.Any + switch family { + case writeReportFamilyEVM: + var wrReq evmcappb.WriteReportRequest + if err = req.Payload.UnmarshalTo(&wrReq); err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal Payload to WriteReportRequest: %w", err) + } + if wrReq.Report == nil { + return [32]byte{}, errors.New("WriteReportRequest.Report is nil") + } + + wrReq.Report.Sigs = nil // exclude signatures from hash + payload, err = anypb.New(&wrReq) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal WriteReportRequest back to anypb: %w", err) + } + case writeReportFamilySolana: + var wrReq solcappb.WriteReportRequest + if err = req.Payload.UnmarshalTo(&wrReq); err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal Payload to WriteReportRequest: %w", err) + } + if wrReq.Report == nil { + return [32]byte{}, errors.New("WriteReportRequest.Report is nil") + } + + wrReq.Report.Sigs = nil // exclude signatures from hash + payload, err = anypb.New(&wrReq) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal WriteReportRequest back to anypb: %w", err) + } + case writeReportFamilyAptos: + var wrReq aptoscappb.WriteReportRequest + if err = req.Payload.UnmarshalTo(&wrReq); err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal Payload to WriteReportRequest: %w", err) + } + if wrReq.Report == nil { + return [32]byte{}, errors.New("WriteReportRequest.Report is nil") + } + + wrReq.Report.Sigs = nil // exclude signatures from hash + payload, err = anypb.New(&wrReq) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal WriteReportRequest back to anypb: %w", err) + } + case writeReportFamilyStellar: + var wrReq stellarcappb.WriteReportRequest + if err = req.Payload.UnmarshalTo(&wrReq); err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal Payload to WriteReportRequest: %w", err) + } + if wrReq.Report == nil { + return [32]byte{}, errors.New("WriteReportRequest.Report is nil") + } + + wrReq.Report.Sigs = nil // exclude signatures from hash + payload, err = anypb.New(&wrReq) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal WriteReportRequest back to anypb: %w", err) + } + default: + return [32]byte{}, fmt.Errorf("unexpected report family: %s", family) + } + + req.Payload = payload + + reqBytes, err := pb.MarshalCapabilityRequest(req) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal capability request: %w", err) + } + return sha256.Sum256(reqBytes), nil +} + +type writeReportFamily string + +var ( + writeReportFamilyEVM writeReportFamily = "evm" + writeReportFamilySolana writeReportFamily = "solana" + writeReportFamilyAptos writeReportFamily = "aptos" + writeReportFamilyStellar writeReportFamily = "stellar" +) + +func getWriteReportFamily(msg *types.MessageBody) (writeReportFamily, error) { + ss := strings.Split(msg.CapabilityId, ":") + if len(ss) < 1 { + return "", errors.New("failed to parse family from capability id") + } + family := ss[0] + switch family { + case "evm": + return writeReportFamilyEVM, nil + case "solana": + return writeReportFamilySolana, nil + case "aptos": + return writeReportFamilyAptos, nil + case "stellar": + return writeReportFamilyStellar, nil + } + + return "", errors.New("report family is unknown, available families: evm, solana, aptos, stellar") +} + +func NewWriteReportExcludeSignaturesHasher() types.MessageHasher { + return &writeReportExcludeSignaturesHasher{} +} diff --git a/libs/x/don2don/executable/hasher_test.go b/libs/x/don2don/executable/hasher_test.go new file mode 100644 index 000000000..73af6c6b5 --- /dev/null +++ b/libs/x/don2don/executable/hasher_test.go @@ -0,0 +1,331 @@ +package executable + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" + solcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/solana" + stellarcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/stellar" + "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" +) + +func TestWriteReportExcludeSignaturesHasher_Hash(t *testing.T) { + t.Parallel() + + req1a := getRequest(t, []byte("testdata"), [][]byte{[]byte("sig1"), []byte("sig2")}) + req1b := getRequest(t, []byte("testdata"), [][]byte{[]byte("sig3"), []byte("sig4")}) + req2 := getRequest(t, []byte("otherdata"), [][]byte{[]byte("sig1"), []byte("sig2")}) + + hasher := NewWriteReportExcludeSignaturesHasher() + hash1a, err := hasher.Hash(req1a) + require.NoError(t, err) + hash1b, err := hasher.Hash(req1b) + require.NoError(t, err) + hash2, err := hasher.Hash(req2) + require.NoError(t, err) + + require.Equal(t, hash1a, hash1b) // same data, different signatures + require.NotEqual(t, hash1a, hash2) // different data, same signatures +} + +func TestWriteReportExcludeSignaturesHasher_Hash_Stellar(t *testing.T) { + t.Parallel() + req1a := getStellarRequest(t, []byte("testdata"), [][]byte{[]byte("sig1"), []byte("sig2")}) + req1b := getStellarRequest(t, []byte("testdata"), [][]byte{[]byte("sig3"), []byte("sig4")}) + req2 := getStellarRequest(t, []byte("otherdata"), [][]byte{[]byte("sig1"), []byte("sig2")}) + + hasher := NewWriteReportExcludeSignaturesHasher() + hash1a, err := hasher.Hash(req1a) + require.NoError(t, err) + hash1b, err := hasher.Hash(req1b) + require.NoError(t, err) + hash2, err := hasher.Hash(req2) + require.NoError(t, err) + + require.Equal(t, hash1a, hash1b) // same data, different signatures + require.NotEqual(t, hash1a, hash2) // different data, same signatures +} + +func getStellarRequest(t *testing.T, rawReport []byte, sigs [][]byte) *types.MessageBody { + attributedSigs := make([]*sdk.AttributedSignature, len(sigs)) + for i, s := range sigs { + attributedSigs[i] = &sdk.AttributedSignature{Signature: s, SignerId: uint32(i)} + } + wrReq := &stellarcappb.WriteReportRequest{ + ContractId: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", + Report: &sdk.ReportResponse{ + RawReport: rawReport, + Sigs: attributedSigs, + }, + } + payload, err := anypb.New(wrReq) + require.NoError(t, err) + capReq := capabilities.CapabilityRequest{Payload: payload} + capReqBytes, err := pb.MarshalCapabilityRequest(capReq) + require.NoError(t, err) + return &types.MessageBody{Payload: capReqBytes, CapabilityId: "stellar:123"} +} + +func TestWriteReportExcludeSignaturesHasher_Hash_NilPayload(t *testing.T) { + t.Parallel() + + nilReq := capabilities.CapabilityRequest{Payload: nil} + nilReqBytes, err := pb.MarshalCapabilityRequest(nilReq) + require.NoError(t, err) + + msgBody := &types.MessageBody{Payload: nilReqBytes} + + hasher := NewWriteReportExcludeSignaturesHasher() + _, err = hasher.Hash(msgBody) + require.Error(t, err) + require.Contains(t, err.Error(), "capability request payload is nil") +} + +func TestWriteReportExcludeSignaturesHasher_Hash_NilReport(t *testing.T) { + t.Parallel() + + nilReq := &evmcappb.WriteReportRequest{Report: nil} + nilReqSol := &solcappb.WriteReportRequest{Report: nil} + nilReqStellar := &stellarcappb.WriteReportRequest{Report: nil} + nilPb, err := anypb.New(nilReq) + require.NoError(t, err) + nilPbSol, err2 := anypb.New(nilReqSol) + capReq := capabilities.CapabilityRequest{Payload: nilPb} + require.NoError(t, err2) + capReqBytes, err3 := pb.MarshalCapabilityRequest(capReq) + require.NoError(t, err3) + capReqSol := capabilities.CapabilityRequest{Payload: nilPbSol} + capReqBytesSol, err4 := pb.MarshalCapabilityRequest(capReqSol) + require.NoError(t, err4) + nilPbStellar, err5 := anypb.New(nilReqStellar) + require.NoError(t, err5) + capReqStellar := capabilities.CapabilityRequest{Payload: nilPbStellar} + capReqBytesStellar, err6 := pb.MarshalCapabilityRequest(capReqStellar) + require.NoError(t, err6) + + msgBodies := []*types.MessageBody{ + {Payload: capReqBytes, CapabilityId: "evm:123"}, + {Payload: capReqBytesSol, CapabilityId: "solana:123"}, + {Payload: capReqBytesStellar, CapabilityId: "stellar:123"}, + } + for _, msgBody := range msgBodies { + hasher := NewWriteReportExcludeSignaturesHasher() + _, err = hasher.Hash(msgBody) + require.Error(t, err) + require.Contains(t, err.Error(), "WriteReportRequest.Report is nil") + } +} + +func TestWriteReportExcludeSignaturesHasher_Hash_InvalidPayload(t *testing.T) { + t.Parallel() + + // Test with completely invalid payload that cannot be unmarshaled + msgBody := &types.MessageBody{ + Payload: []byte("invalid protobuf data"), + } + + hasher := NewWriteReportExcludeSignaturesHasher() + _, err := hasher.Hash(msgBody) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to unmarshal capability request") +} + +func TestSimpleHasher_ExcludesSpendLimits(t *testing.T) { + t.Parallel() + + // Create two requests with identical payloads but different SpendLimits + req1 := getRequestWithSpendLimits(t, []byte("testdata"), []capabilities.SpendLimit{ + {SpendType: "gas", Limit: "1000"}, + {SpendType: "compute", Limit: "500"}, + }) + req2 := getRequestWithSpendLimits(t, []byte("testdata"), []capabilities.SpendLimit{ + {SpendType: "gas", Limit: "2000"}, + {SpendType: "compute", Limit: "1000"}, + }) + req3 := getRequestWithSpendLimits(t, []byte("otherdata"), []capabilities.SpendLimit{ + {SpendType: "gas", Limit: "1000"}, + {SpendType: "compute", Limit: "500"}, + }) + + hasher := NewSimpleHasher() + hash1, err := hasher.Hash(req1) + require.NoError(t, err) + hash2, err := hasher.Hash(req2) + require.NoError(t, err) + hash3, err := hasher.Hash(req3) + require.NoError(t, err) + + require.Equal(t, hash1, hash2) // same data, different SpendLimits should produce same hash + require.NotEqual(t, hash1, hash3) // different data should produce different hash +} + +func TestSimpleHasher_ExcludesExecutionTimestamp(t *testing.T) { + t.Parallel() + + ts1 := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) + ts2 := time.Date(2025, 7, 20, 8, 30, 0, 0, time.UTC) + req1 := getRequestWithMetadata(t, []byte("testdata"), capabilities.RequestMetadata{ + WorkflowID: "wf1", WorkflowExecutionID: "exec1", ExecutionTimestamp: ts1, + }) + req2 := getRequestWithMetadata(t, []byte("testdata"), capabilities.RequestMetadata{ + WorkflowID: "wf1", WorkflowExecutionID: "exec1", ExecutionTimestamp: ts2, + }) + req3 := getRequestWithMetadata(t, []byte("otherdata"), capabilities.RequestMetadata{ + WorkflowID: "wf1", WorkflowExecutionID: "exec1", ExecutionTimestamp: ts1, + }) + + hasher := NewSimpleHasher() + hash1, err := hasher.Hash(req1) + require.NoError(t, err) + hash2, err := hasher.Hash(req2) + require.NoError(t, err) + hash3, err := hasher.Hash(req3) + require.NoError(t, err) + + require.Equal(t, hash1, hash2) // same data, different ExecutionTimestamp should produce same hash + require.NotEqual(t, hash1, hash3) // different data should produce different hash +} + +func TestWriteReportExcludeSignaturesHasher_ExcludesSpendLimits(t *testing.T) { + t.Parallel() + + // Create two requests with identical payloads but different SpendLimits + req1 := getWriteReportRequestWithSpendLimits(t, []byte("testdata"), [][]byte{[]byte("sig1"), []byte("sig2")}, []capabilities.SpendLimit{ + {SpendType: "gas", Limit: "1000"}, + {SpendType: "compute", Limit: "500"}, + }) + req2 := getWriteReportRequestWithSpendLimits(t, []byte("testdata"), [][]byte{[]byte("sig3"), []byte("sig4")}, []capabilities.SpendLimit{ + {SpendType: "gas", Limit: "2000"}, + {SpendType: "compute", Limit: "1000"}, + }) + req3 := getWriteReportRequestWithSpendLimits(t, []byte("otherdata"), [][]byte{[]byte("sig1"), []byte("sig2")}, []capabilities.SpendLimit{ + {SpendType: "gas", Limit: "1000"}, + {SpendType: "compute", Limit: "500"}, + }) + + hasher := NewWriteReportExcludeSignaturesHasher() + hash1, err := hasher.Hash(req1) + require.NoError(t, err) + hash2, err := hasher.Hash(req2) + require.NoError(t, err) + hash3, err := hasher.Hash(req3) + require.NoError(t, err) + + require.Equal(t, hash1, hash2) // same data, different SpendLimits and signatures should produce same hash + require.NotEqual(t, hash1, hash3) // different data should produce different hash +} + +func getRequest(t *testing.T, data []byte, sigs [][]byte) *types.MessageBody { + attrSigs := make([]*sdk.AttributedSignature, len(sigs)) + for i, sig := range sigs { + attrSigs[i] = &sdk.AttributedSignature{ + Signature: sig, + SignerId: uint32(i), + } + } + report := &sdk.ReportResponse{ + RawReport: data, + Sigs: attrSigs, + } + wrReq := &evmcappb.WriteReportRequest{ + Report: report, + } + wrAny, err := anypb.New(wrReq) + require.NoError(t, err) + capReq := capabilities.CapabilityRequest{ + Payload: wrAny, + } + capReqBytes, err := pb.MarshalCapabilityRequest(capReq) + require.NoError(t, err) + return &types.MessageBody{ + Payload: capReqBytes, + CapabilityId: "evm:123", + } +} + +func getRequestWithMetadata(t *testing.T, data []byte, md capabilities.RequestMetadata) *types.MessageBody { + report := &sdk.ReportResponse{ + RawReport: data, + Sigs: []*sdk.AttributedSignature{}, + } + wrReq := &evmcappb.WriteReportRequest{ + Report: report, + } + wrAny, err := anypb.New(wrReq) + require.NoError(t, err) + capReq := capabilities.CapabilityRequest{ + Payload: wrAny, + Metadata: md, + } + capReqBytes, err := pb.MarshalCapabilityRequest(capReq) + require.NoError(t, err) + return &types.MessageBody{ + Payload: capReqBytes, + } +} + +func getRequestWithSpendLimits(t *testing.T, data []byte, spendLimits []capabilities.SpendLimit) *types.MessageBody { + report := &sdk.ReportResponse{ + RawReport: data, + Sigs: []*sdk.AttributedSignature{}, + } + wrReq := &evmcappb.WriteReportRequest{ + Report: report, + } + wrAny, err := anypb.New(wrReq) + require.NoError(t, err) + capReq := capabilities.CapabilityRequest{ + Payload: wrAny, + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-workflow", + WorkflowExecutionID: "test-execution", + SpendLimits: spendLimits, + }, + } + capReqBytes, err := pb.MarshalCapabilityRequest(capReq) + require.NoError(t, err) + return &types.MessageBody{ + Payload: capReqBytes, + } +} + +func getWriteReportRequestWithSpendLimits(t *testing.T, data []byte, sigs [][]byte, spendLimits []capabilities.SpendLimit) *types.MessageBody { + attrSigs := make([]*sdk.AttributedSignature, len(sigs)) + for i, sig := range sigs { + attrSigs[i] = &sdk.AttributedSignature{ + Signature: sig, + SignerId: uint32(i), + } + } + report := &sdk.ReportResponse{ + RawReport: data, + Sigs: attrSigs, + } + wrReq := &evmcappb.WriteReportRequest{ + Report: report, + } + wrAny, err := anypb.New(wrReq) + require.NoError(t, err) + capReq := capabilities.CapabilityRequest{ + Payload: wrAny, + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-workflow", + WorkflowExecutionID: "test-execution", + SpendLimits: spendLimits, + }, + } + capReqBytes, err := pb.MarshalCapabilityRequest(capReq) + require.NoError(t, err) + return &types.MessageBody{ + Payload: capReqBytes, + CapabilityId: "evm:2321", + } +} diff --git a/libs/x/don2don/executable/request/client_request.go b/libs/x/don2don/executable/request/client_request.go new file mode 100644 index 000000000..9f0ec3086 --- /dev/null +++ b/libs/x/don2don/executable/request/client_request.go @@ -0,0 +1,618 @@ +package request + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "sort" + "sync" + "time" + + "google.golang.org/protobuf/proto" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/durableemitter" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-protos/workflows/go/events" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + "github.com/smartcontractkit/capabilities/libs/x/don2don/transmission" + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/don2don/validation" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// errRemoteCapabilityExecuteError preserves the legacy "TRANSPORT : ErrorMsg" string from the +// remote executable client while wrapping a deserialized caperrors.Error so callers can +// errors.As into caperrors.Error after RPC (see capability_executor metrics). +type errRemoteCapabilityExecuteError struct { + s string + wrap caperrors.Error +} + +func (e *errRemoteCapabilityExecuteError) Error() string { return e.s } + +func (e *errRemoteCapabilityExecuteError) Unwrap() error { return e.wrap } + +func newRemoteCapabilityExecuteError(transportErr types.Error, errMsg string) error { + return &errRemoteCapabilityExecuteError{ + s: fmt.Sprintf("%s : %s", transportErr, errMsg), + wrap: caperrors.DeserializeErrorFromString(errMsg), + } +} + +func newRemoteCapabilityExecuteErrorWithMessage(display string, errMsg string) error { + return &errRemoteCapabilityExecuteError{ + s: display, + wrap: caperrors.DeserializeErrorFromString(errMsg), + } +} + +// newRemoteCapabilityExecuteErrorFromCapError wraps a capability error produced locally +// (not deserialized from a remote payload). Unwrap returns caperrors.Error so +// capability_executor can errors.As into caperrors.Error. +func newRemoteCapabilityExecuteErrorFromCapError(capErr caperrors.Error) error { + return &errRemoteCapabilityExecuteError{ + s: capErr.Error(), + wrap: capErr, + } +} + +// ErrResponseQuorumUnreachable is returned when enough peer responses have been +// received that the required number of matching payloads (configurable; defaults to F+1) are no longer mathematically possible. +var ErrResponseQuorumUnreachable = errors.New("response quorum unreachable: not enough matching capability responses") + +type clientResponse struct { + Result []byte + Err error +} + +type ClientRequest struct { + id string + cancelFn context.CancelFunc + responseCh chan clientResponse + createdAt time.Time + responseIDCount map[[32]byte]int + meteringResponses map[[32]byte][]commoncap.MeteringNodeDetail + errorCount map[string]int + totalErrorCount int + payloadNotAvailableCount int + responseReceived map[rage.PeerID]bool + lggr logger.Logger + signers [][]byte + workflowExecutionID string + referenceID string + + requiredResponseConfirmations int + remoteNodeCount int + remoteDonF int + + requestTimeout time.Duration + + respSent bool + mux sync.Mutex + wg *sync.WaitGroup +} + +// TransmissionConfig has to be set only for V2 capabilities. V1 capabilities read transmission schedule from every request. +func NewClientExecuteRequest(ctx context.Context, lggr logger.Logger, req commoncap.CapabilityRequest, + remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, dispatcher types.Dispatcher, + requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig, capMethodName string, + signers [][]byte, minResponsesToAggregate uint32, +) (*ClientRequest, error) { + rawRequest, err := proto.MarshalOptions{Deterministic: true}.Marshal(pb.CapabilityRequestToProto(req)) + if err != nil { + return nil, fmt.Errorf("failed to marshal capability request: %w", err) + } + + workflowExecutionID := req.Metadata.WorkflowExecutionID + if err = validation.ValidateWorkflowOrExecutionID(workflowExecutionID); err != nil { + return nil, fmt.Errorf("workflow execution ID is invalid: %w", err) + } + + // the requestID must be delineated by the workflow execution ID and the reference ID + // to ensure that it supports parallel step execution + requestID := types.MethodExecute + ":" + workflowExecutionID + ":" + req.Metadata.ReferenceID + + var tc transmission.TransmissionConfig + if transmissionConfig != nil { + // all v2 capabilities should be all at once + tc = transmission.TransmissionConfig{ + Schedule: transmission.Schedule_AllAtOnce, + } + } else { // per-workflow setting used by V1 Capabilities + tc, err = transmission.ExtractTransmissionConfig(req.Config) + if err != nil { + return nil, fmt.Errorf("failed to extract transmission config from request: %w", err) + } + } + + lggr = logger.With(lggr, "requestId", requestID) // cap ID and method name included in the parent logger + return newClientRequest(ctx, lggr, requestID, remoteCapabilityInfo, localDonInfo, dispatcher, requestTimeout, tc, types.MethodExecute, rawRequest, workflowExecutionID, req.Metadata.ReferenceID, capMethodName, signers, minResponsesToAggregate) +} + +var ( + defaultDelayMargin = 10 * time.Second + // Extra time the workflow DON client waits beyond requestTimeout for P2P responses + // after remote capability nodes hit their execution deadline. + defaultResponseAggregationGrace = 10 * time.Second +) + +// requiredConfirmations returns the minimum number of matching peer responses needed before +// the workflow DON accepts a read result. When minResponsesToAggregate is non-zero it is used +// directly; otherwise the default of F+1 is used (backward compatible). +func requiredConfirmations(f uint8, minResponsesToAggregate uint32) int { + if minResponsesToAggregate > 0 { + return int(minResponsesToAggregate) + } + return int(f) + 1 +} + +func newClientRequest(ctx context.Context, lggr logger.Logger, requestID string, remoteCapabilityInfo commoncap.CapabilityInfo, + localDonInfo commoncap.DON, dispatcher types.Dispatcher, requestTimeout time.Duration, + tc transmission.TransmissionConfig, methodType string, rawRequest []byte, workflowExecutionID string, stepRef string, capMethodName string, + signers [][]byte, minResponsesToAggregate uint32, +) (*ClientRequest, error) { + remoteCapabilityDonInfo := remoteCapabilityInfo.DON + if remoteCapabilityDonInfo == nil { + return nil, errors.New("remote capability info missing DON") + } + + peerIDToTransmissionDelay, err := transmission.GetPeerIDToTransmissionDelaysForConfig(remoteCapabilityDonInfo.Members, requestID, tc) + if err != nil { + return nil, fmt.Errorf("failed to get peer ID to transmission delay: %w", err) + } + + // send schedule through beholder for single execution performance tracking + err = emitTransmissionScheduleEvent(ctx, + tc.Schedule, + workflowExecutionID, + requestID, + remoteCapabilityInfo.ID, + stepRef, + peerIDToTransmissionDelay, + ) + if err != nil { + lggr.Errorw("failed to emit transmission schedule event", "error", err) + } + + responseReceived := make(map[rage.PeerID]bool) + + maxDelayDuration := time.Duration(0) + for _, delay := range peerIDToTransmissionDelay { + if delay > maxDelayDuration { + maxDelayDuration = delay + } + } + + // Add some margin to allow the last peer to respond + maxDelayDuration += defaultDelayMargin + + // Instantiate a new context based on the parent, but without its deadline. + // We set a new deadline instead equal to the original timeout OR the full length + // of the execution schedule plus some margin, whichever is greater + + // We do this to ensure that we will always execute the entire transmission schedule. + // This ensures that all capability DON nodes will receive a quorum of requests, + // and will execute all requests they receive from the workflow DON, preventing + // quorum errors from lagging members of the workflow DON. + dl, ok := ctx.Deadline() + originalTimeout := time.Duration(0) + if ok { + originalTimeout = time.Until(dl) + } + effectiveTimeout := max(originalTimeout, maxDelayDuration) + + // Now let's create a new context based on the adjusted timeout value. + // By calling WithoutCancel, we ensure that this context can only be cancelled in + // one of two ways -- 1) by explicitly calling the cancelFn we create below, or 2) + // after the adjusted timeout expires. + ctxWithoutCancel := context.WithoutCancel(ctx) + ctxWithCancel, cancelFn := context.WithTimeout(ctxWithoutCancel, effectiveTimeout) + + lggr.Debugw("sending request to peers", "schedule", peerIDToTransmissionDelay, "originalTimeout", originalTimeout, "effectiveTimeout", effectiveTimeout) + + var wg sync.WaitGroup + for peerID, delay := range peerIDToTransmissionDelay { + responseReceived[peerID] = false + + wg.Add(1) + go func(innerCtx context.Context, peerID ragetypes.PeerID, delay time.Duration) { + defer wg.Done() + message := &types.MessageBody{ + CapabilityId: remoteCapabilityInfo.ID, + CapabilityDonId: remoteCapabilityDonInfo.ID, + CallerDonId: localDonInfo.ID, + Method: methodType, + Payload: rawRequest, + MessageId: []byte(requestID), + CapabilityMethod: capMethodName, + } + + timer := time.NewTimer(delay) + defer timer.Stop() + + select { + case <-innerCtx.Done(): + lggr.Debugw("context done, not sending request to peer", "peerID", peerID) + return + case <-timer.C: + lggr.Debugw("sending request to peer", "peerID", peerID) + err := dispatcher.Send(peerID, message) + if err != nil { + lggr.Errorw("failed to send message", "peerID", peerID, "error", err) + } + } + }(ctxWithCancel, peerID, delay) + } + + return &ClientRequest{ + id: requestID, + cancelFn: cancelFn, + createdAt: time.Now(), + requestTimeout: requestTimeout, + requiredResponseConfirmations: requiredConfirmations(remoteCapabilityDonInfo.F, minResponsesToAggregate), + remoteNodeCount: len(remoteCapabilityDonInfo.Members), + remoteDonF: int(remoteCapabilityDonInfo.F), + responseIDCount: make(map[[32]byte]int), + meteringResponses: make(map[[32]byte][]commoncap.MeteringNodeDetail), + errorCount: make(map[string]int), + responseReceived: responseReceived, + responseCh: make(chan clientResponse, 1), + wg: &wg, + lggr: lggr, + signers: signers, + workflowExecutionID: workflowExecutionID, + referenceID: stepRef, + }, nil +} + +func emitTransmissionScheduleEvent(ctx context.Context, scheduleType, workflowExecutionID, transmissionID, capabilityID, stepRef string, peerIDToTransmissionDelay map[rage.PeerID]time.Duration) error { + // Create a slice of peer IDs sorted by their delay values + type peerDelay struct { + peerID rage.PeerID + delay time.Duration + } + + peerDelays := make([]peerDelay, 0, len(peerIDToTransmissionDelay)) + for peerID, delay := range peerIDToTransmissionDelay { + peerDelays = append(peerDelays, peerDelay{peerID, delay}) + } + + // Sort by delay value + sort.Slice(peerDelays, func(i, j int) bool { + return peerDelays[i].delay < peerDelays[j].delay + }) + + // Create map with sorted peers and their delays in milliseconds + peerDelaysMap := make(map[string]int64, len(peerDelays)) + for _, pd := range peerDelays { + peerDelaysMap[pd.peerID.String()] = pd.delay.Milliseconds() + } + + msg := &events.TransmissionsScheduledEvent{ + Timestamp: time.Now().Format(time.RFC3339), + ScheduleType: scheduleType, + WorkflowExecutionID: workflowExecutionID, + TransmissionID: transmissionID, + CapabilityID: capabilityID, + StepRef: stepRef, + PeerTransmissionDelays: peerDelaysMap, + } + + b, err := proto.Marshal(msg) + if err != nil { + return fmt.Errorf("failed to marshal TransmissionScheduleEvent: %w", err) + } + + // emit transmission schedule event to track which nodes are successful when called to emit + entity := fmt.Sprintf("%s.%s", TransmissionEventProtoPkg, TransmissionEventEntity) + if err = beholder.GetEmitter().Emit(ctx, b, + "beholder_data_schema", TransmissionEventSchema, // required + "beholder_domain", "platform", // required + "beholder_entity", entity); err != nil { // required + return err + } + + err = durableemitter.GlobalEmit(ctx, b, "source", "platform", "type", entity) + if err != nil && !errors.Is(err, durableemitter.ErrNotInitialized) { + return err + } + return nil +} + +func (c *ClientRequest) ID() string { + return c.id +} + +func (c *ClientRequest) ResponseChan() <-chan clientResponse { + return c.responseCh +} + +func (c *ClientRequest) Expired() bool { + return time.Since(c.createdAt) > c.requestTimeout+defaultResponseAggregationGrace +} + +func (c *ClientRequest) Cancel(err error) { + c.cancelFn() + c.wg.Wait() + c.mux.Lock() + defer c.mux.Unlock() + if !c.respSent { + c.sendResponse(clientResponse{Err: err}) + } +} + +func (c *ClientRequest) OnMessage(_ context.Context, msg *types.MessageBody) error { + c.mux.Lock() + defer c.mux.Unlock() + + if c.respSent { + return nil + } + + if msg.Sender == nil { + return errors.New("sender missing from message") + } + + c.lggr.Debugw("OnMessage called for client request") + + sender, err := don2don.ToPeerID(msg.Sender) + if err != nil { + return fmt.Errorf("failed to convert message sender to PeerID: %w", err) + } + + received, expected := c.responseReceived[sender] + if !expected { + return fmt.Errorf("response from peer %s not expected", sender) + } + + if received { + return fmt.Errorf("response from peer %s already received", sender) + } + + c.responseReceived[sender] = true + + if msg.Error == types.Error_OK { + resp, err := pb.UnmarshalCapabilityResponse(msg.Payload) + if err != nil { + return fmt.Errorf("failed to unmarshal capability response: %w", err) + } + + // metering reports per node are aggregated into a single array of values. for any single node message, the + // metering values are extracted from the CapabilityResponse, added to an array, and the CapabilityResponse + // is marshalled without the metering value to get the hash. each node could have a different metering value + // which would result in different hashes. removing the metering detail allows for direct comparison of results. + responseID, err := c.getMessageHash(resp) + if err != nil { + return fmt.Errorf("failed to get message hash: %w", err) + } + + lggr := logger.With(c.lggr, "responseID", hex.EncodeToString(responseID[:]), "requiredCount", c.requiredResponseConfirmations, "peer", sender) + + nodeReports, exists := c.meteringResponses[responseID] + if !exists { + nodeReports = make([]commoncap.MeteringNodeDetail, 0) + } + + rpt, err := commoncap.ExtractMeteringFromMetadata(sender, resp.Metadata) + if err != nil { + lggr.Warnw("invalid metering detail", "err", err) + } else { + nodeReports = append(nodeReports, rpt) + } + + c.responseIDCount[responseID]++ + c.meteringResponses[responseID] = nodeReports + + if len(c.responseIDCount) > 1 { + lggr.Warnw("received multiple unique responses for the same request", "count for responseID", len(c.responseIDCount)) + } + + if c.responseIDCount[responseID] == c.requiredResponseConfirmations || c.hasValidAttestation(resp) { + payload, err := c.encodePayloadWithMetadata(msg, commoncap.ResponseMetadata{Metering: nodeReports}) + if err != nil { + return fmt.Errorf("failed to encode payload with metadata: %w", err) + } + + c.sendResponse(clientResponse{Result: payload}) + } else { + c.trySendQuorumUnreachableError() + } + } else { + c.lggr.Debugw("received error from peer", "error", msg.Error, "errorMsg", msg.ErrorMsg, "peer", sender) + if commoncap.ErrResponsePayloadNotAvailable.Is(errors.New(msg.ErrorMsg)) { + c.payloadNotAvailableCount++ + if c.payloadNotAvailableCount == c.remoteNodeCount-c.requiredResponseConfirmations+1 { + // return an error to indicate unexpected state, but do not send an error as we might still receive a response with valid attestation. + return fmt.Errorf("unexpected state: received %d payload not available responses, while max allowed is %d. This means a bug in the code, please investigate", + c.payloadNotAvailableCount, c.remoteNodeCount-c.requiredResponseConfirmations) + } + return nil + } + + c.errorCount[msg.ErrorMsg]++ + c.totalErrorCount++ + + if len(c.errorCount) > 1 { + c.lggr.Warnw("received multiple different errors for the same request", "numDifferentErrors", len(c.errorCount)) + } + + if c.errorCount[msg.ErrorMsg] == c.requiredResponseConfirmations { + c.sendResponse(clientResponse{Err: newRemoteCapabilityExecuteError(msg.Error, msg.ErrorMsg)}) + } else if c.totalErrorCount == c.remoteNodeCount-c.requiredResponseConfirmations+1 { + c.sendResponse(clientResponse{Err: newRemoteCapabilityExecuteErrorWithMessage( + fmt.Sprintf("received %d errors, last error %s : %s", c.totalErrorCount, msg.Error, msg.ErrorMsg), + msg.ErrorMsg, + )}) + } + } + return nil +} + +func (c *ClientRequest) responsesReceivedCount() int { + n := 0 + for _, received := range c.responseReceived { + if received { + n++ + } + } + return n +} + +func (c *ClientRequest) maxMatchingResponseCount() int { + currentMax := 0 + for _, count := range c.responseIDCount { + if count > currentMax { + currentMax = count + } + } + return currentMax +} + +// quorumStillPossible reports whether requiredResponseConfirmations identical OK +// responses can still be reached. Each pending peer can add at most one vote to +// the current leading payload hash. +func (c *ClientRequest) quorumStillPossible(pending int) bool { + return c.maxMatchingResponseCount()+pending >= c.requiredResponseConfirmations +} + +func (c *ClientRequest) trySendQuorumUnreachableError() { + pending := c.pending() + if c.respSent || c.quorumStillPossible(pending) { + return + } + received := c.responsesReceivedCount() + unique := len(c.responseIDCount) + maxMatch := c.maxMatchingResponseCount() + + detail := fmt.Errorf( + "received %d/%d peer responses with %d unique payloads; best match count %d, need %d (%d responses pending)", + received, c.remoteNodeCount, unique, maxMatch, c.requiredResponseConfirmations, pending, + ) + capErr := caperrors.NewPublicSystemError( + fmt.Errorf("%w: %w", ErrResponseQuorumUnreachable, detail), + caperrors.ConsensusFailed, + ) + c.lggr.Warnw("response quorum unreachable, failing early", + "responsesReceived", received, + "remoteNodeCount", c.remoteNodeCount, + "uniqueResponseCount", unique, + "maxMatchingResponseCount", maxMatch, + "requiredResponseConfirmations", c.requiredResponseConfirmations, + "pendingResponses", pending, + ) + c.sendResponse(clientResponse{Err: newRemoteCapabilityExecuteErrorFromCapError(capErr)}) +} + +func (c *ClientRequest) pending() int { + received := c.responsesReceivedCount() + pending := c.remoteNodeCount - received + return pending +} + +func (c *ClientRequest) hasValidAttestation(resp commoncap.CapabilityResponse) bool { + if resp.OCRAttestation == nil { + return false + } + + // report_attestation.go in libocr only ever collects F+1 signatures before declaring a report complete + // (it stops as soon as it has one signature past the DON's F threshold). That means an OCRAttestation + // can never carry more than F+1 signatures, regardless of how the capability DON is configured. + // When requiredResponseConfirmations (driven by minResponsesToAggregate) is set above F+1, verifyAttestation + // would therefore always fail and log a spurious "this is most likely a bug" error on every response. In that + // case we deliberately skip attestation verification altogether and fall back to the identical-response quorum check instead. + if c.requiredResponseConfirmations > c.remoteDonF+1 { + return false + } + + err := c.verifyAttestation(resp) + if err != nil { + c.lggr.Errorw("Attestation is present, but not valid. This is most likely a bug and requires investigation - falling back to identical responses verification", "error", err) + return false + } + + return true +} + +func (c *ClientRequest) verifyAttestation(resp commoncap.CapabilityResponse) error { + attestation := resp.OCRAttestation + if attestation == nil { + return errors.New("attestation is missing") + } + + if len(attestation.Sigs) < c.requiredResponseConfirmations { + return fmt.Errorf("not enough signatures: got %d, need at least %d", len(attestation.Sigs), c.requiredResponseConfirmations) + } + + if len(c.signers) < c.requiredResponseConfirmations { + return fmt.Errorf("number of configured OCR signers is less than required confirmations: got %d, need at least %d", len(c.signers), c.requiredResponseConfirmations) + } + + reportData, err := commoncap.ResponseToReportData(c.workflowExecutionID, c.referenceID, resp.Payload.Value, resp.Metadata) + if err != nil { + return fmt.Errorf("failed to convert response to report data: %w", err) + } + sigData := ocr2key.ReportToSigData3(attestation.ConfigDigest, attestation.SequenceNumber, reportData[:]) + signed := make([]bool, len(c.signers)) + for _, sig := range attestation.Sigs { + if int(sig.Signer) >= len(c.signers) { + return fmt.Errorf("invalid signer index: %d", sig.Signer) + } + + if signed[sig.Signer] { + return fmt.Errorf("duplicate signature from signer index: %d", sig.Signer) + } + + if !ocr2key.EvmVerifyBlob(c.signers[sig.Signer], sigData, sig.Signature) { + return fmt.Errorf("invalid signature from signer index: %d", sig.Signer) + } + + signed[sig.Signer] = true + } + + return nil +} + +func (c *ClientRequest) sendResponse(response clientResponse) { + c.responseCh <- response + close(c.responseCh) + c.respSent = true + if response.Err != nil { + c.lggr.Warnw("received error response", "error", don2don.SanitizeLogString(response.Err.Error())) + return + } + c.lggr.Debugw("received OK response") +} + +func (c *ClientRequest) getMessageHash(msg commoncap.CapabilityResponse) ([32]byte, error) { + // clear metadata to ensure it doesn't affect the hash, as different nodes might have different metadata (e.g. different metering values) + // since msg is passed as value, this won't affect the original message + msg.Metadata = commoncap.ResponseMetadata{} + msg.OCRAttestation = nil + payload, err := pb.MarshalCapabilityResponse(msg) + if err != nil { + return [32]byte{}, err + } + + return sha256.Sum256(payload), nil +} + +func (c *ClientRequest) encodePayloadWithMetadata(msg *types.MessageBody, metadata commoncap.ResponseMetadata) ([]byte, error) { + resp, err := pb.UnmarshalCapabilityResponse(msg.Payload) + if err != nil { + return nil, err + } + + resp.Metadata = metadata + + return pb.MarshalCapabilityResponse(resp) +} diff --git a/libs/x/don2don/executable/request/server_request.go b/libs/x/don2don/executable/request/server_request.go new file mode 100644 index 000000000..75198596d --- /dev/null +++ b/libs/x/don2don/executable/request/server_request.go @@ -0,0 +1,394 @@ +package request + +import ( + "context" + "errors" + "fmt" + "slices" + "strconv" + "sync" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +type srMetrics struct { + capabilityID string + callingDonID string + executeDuration metric.Int64Histogram + executeCount metric.Int64Counter + executeRequestCount metric.Int64Counter + executeResponseCount metric.Int64Counter +} + +func (s *srMetrics) recordExecutionDuration(ctx context.Context, d time.Duration, success bool) { + successStr := "false" + if success { + successStr = "true" + } + s.executeDuration.Record(ctx, d.Milliseconds(), metric.WithAttributes( + attribute.String("success", successStr), attribute.String("callingDON", s.callingDonID), attribute.String("capabilityID", s.capabilityID), + )) +} + +func (s *srMetrics) countExecution(ctx context.Context, success bool) { + successStr := "false" + if success { + successStr = "true" + } + s.executeCount.Add(ctx, 1, metric.WithAttributes( + attribute.String("success", successStr), attribute.String("callingDON", s.callingDonID), attribute.String("capabilityID", s.capabilityID), + )) +} + +func (s *srMetrics) countExecutionRequest(ctx context.Context) { + s.executeRequestCount.Add(ctx, 1, metric.WithAttributes( + attribute.String("callingDON", s.callingDonID), attribute.String("capabilityID", s.capabilityID), + )) +} + +func (s *srMetrics) countExecutionResponse(ctx context.Context, status string, dispatcherErr bool) { + // Beholder doesn't support non-string attributes + dv := "false" + if dispatcherErr { + dv = "true" + } + s.executeResponseCount.Add( + ctx, 1, + metric.WithAttributes(attribute.String("callingDON", s.callingDonID), attribute.String("capabilityID", s.capabilityID), attribute.String("status", status), attribute.String("dispatcherErr", dv))) +} + +func newSrMetrics(capabilityID string, callingDonID uint32) (*srMetrics, error) { + h, err := beholder.GetMeter().Int64Histogram("platform_executable_capability_server_execute_duration_ms") + if err != nil { + return nil, err + } + + ec, err := beholder.GetMeter().Int64Counter("platform_executable_capability_server_execute_count") + if err != nil { + return nil, err + } + + erc, err := beholder.GetMeter().Int64Counter("platform_executable_capability_server_execute_request_count") + if err != nil { + return nil, err + } + + erspc, err := beholder.GetMeter().Int64Counter("platform_executable_capability_server_execute_response_count") + if err != nil { + return nil, err + } + + return &srMetrics{ + capabilityID: capabilityID, + callingDonID: strconv.FormatUint(uint64(callingDonID), 10), + executeDuration: h, + executeCount: ec, + executeRequestCount: erc, + executeResponseCount: erspc, + }, nil +} + +type response struct { + response []byte + error types.Error + errorMsg string +} + +type ServerRequest struct { + capability capabilities.ExecutableCapability + + capabilityPeerID rage.PeerID + capabilityID string + capabilityDonID uint32 + + dispatcher types.Dispatcher + + requesters map[rage.PeerID]bool + responseSentToRequester map[rage.PeerID]bool + + createdTime time.Time + + response *response + + callingDon capabilities.DON + + requestMessageID string + method string + requestTimeout time.Duration + capMethodName string + + // workflowDONBindingGate, when open, requires the request's + // Metadata.WorkflowDonID to match the authenticated calling DON. + workflowDONBindingGate limits.GateLimiter + + mux sync.Mutex + lggr logger.Logger + + metrics *srMetrics +} + +func NewServerRequest(capability capabilities.ExecutableCapability, method string, capabilityID string, capabilityDonID uint32, + capabilityPeerID rage.PeerID, + callingDon capabilities.DON, requestID string, + dispatcher types.Dispatcher, requestTimeout time.Duration, capMethodName string, + workflowDONBindingGate limits.GateLimiter, lggr logger.Logger) (*ServerRequest, error) { + lggr = logger.With(logger.Named(lggr, "ServerRequest"), "requestID", requestID) // cap ID and method name included in the parent logger + + if workflowDONBindingGate == nil { + return nil, errors.New("workflowDONBindingGate must not be nil") + } + + m, err := newSrMetrics(capabilityID, callingDon.ID) + if err != nil { + return nil, err + } + + return &ServerRequest{ + capability: capability, + createdTime: time.Now(), + capabilityID: capabilityID, + capabilityDonID: capabilityDonID, + capabilityPeerID: capabilityPeerID, + dispatcher: dispatcher, + requesters: map[rage.PeerID]bool{}, + responseSentToRequester: map[rage.PeerID]bool{}, + callingDon: callingDon, + requestMessageID: requestID, + method: method, + requestTimeout: requestTimeout, + capMethodName: capMethodName, + workflowDONBindingGate: workflowDONBindingGate, + lggr: lggr, + metrics: m, + }, nil +} + +func (e *ServerRequest) OnMessage(ctx context.Context, msg *types.MessageBody) error { + e.metrics.countExecutionRequest(ctx) + + e.mux.Lock() + defer e.mux.Unlock() + + if msg.Sender == nil { + return errors.New("sender missing from message") + } + + requester, err := don2don.ToPeerID(msg.Sender) + if err != nil { + return fmt.Errorf("failed to convert message sender to PeerID: %w", err) + } + + if err := e.addRequester(requester); err != nil { + return fmt.Errorf("failed to add requester to request: %w", err) + } + + e.lggr.Debugw("OnMessage called for request", "calls", len(e.requesters), + "hasResponse", e.response != nil, "requester", requester.String(), "minRequsters", e.callingDon.F+1) + + if e.minimumRequiredRequestsReceived() && !e.hasResponse() { + switch e.method { + case types.MethodExecute: + e.executeRequest(ctx, msg, executeCapabilityRequest) + default: + e.setError(types.Error_INTERNAL_ERROR, "unknown method %s"+e.method) + } + } + + if err := e.sendResponses(ctx); err != nil { + return fmt.Errorf("failed to send responses: %w", err) + } + + return nil +} + +func (e *ServerRequest) Expired() bool { + return time.Since(e.createdTime) > e.requestTimeout +} + +func (e *ServerRequest) Evictable(minRetention time.Duration) bool { + age := time.Since(e.createdTime) + return age > e.requestTimeout && age > minRetention +} + +func (e *ServerRequest) Cancel(ctx context.Context, err types.Error, msg string) error { + e.mux.Lock() + defer e.mux.Unlock() + + if !e.hasResponse() { + e.setError(err, msg) + if err := e.sendResponses(ctx); err != nil { + return fmt.Errorf("failed to send responses: %w", err) + } + } + + return nil +} + +type executeFn func(ctx context.Context, lggr logger.Logger, capability capabilities.ExecutableCapability, payload []byte, callingDonID uint32, workflowDONBindingGate limits.GateLimiter) ([]byte, error) + +func (e *ServerRequest) executeRequest(ctx context.Context, msg *types.MessageBody, method executeFn) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, e.requestTimeout) + defer cancel() + + success := false + start := time.Now() + responsePayload, err := method(ctxWithTimeout, e.lggr, e.capability, msg.Payload, e.callingDon.ID, e.workflowDONBindingGate) + if err != nil { + e.setError(types.Error_INTERNAL_ERROR, err.Error()) + } else { + success = true + e.setResult(responsePayload) + } + + e.metrics.countExecution(ctx, success) + e.metrics.recordExecutionDuration(ctx, time.Since(start), success) +} + +func (e *ServerRequest) addRequester(from rage.PeerID) error { + fromPeerInCallingDon := slices.Contains(e.callingDon.Members, from) + + if !fromPeerInCallingDon { + return fmt.Errorf("request received from peer %s not in calling don", from) + } + + if e.requesters[from] { + return fmt.Errorf("request already received from peer %s", from) + } + + e.requesters[from] = true + + return nil +} + +func (e *ServerRequest) minimumRequiredRequestsReceived() bool { + return len(e.requesters) >= int(e.callingDon.F+1) +} + +func (e *ServerRequest) setResult(result []byte) { + e.lggr.Debug("setting result on request") + e.response = &response{ + response: result, + } +} + +func (e *ServerRequest) setError(err types.Error, errMsg string) { + e.lggr.Debugw("setting error on request", "type", err, "error", errMsg) + e.response = &response{ + error: err, + errorMsg: errMsg, + } +} + +func (e *ServerRequest) hasResponse() bool { + return e.response != nil +} + +func (e *ServerRequest) sendResponses(ctx context.Context) error { + if e.hasResponse() { + for requester := range e.requesters { + if !e.responseSentToRequester[requester] { + e.responseSentToRequester[requester] = true + if err := e.sendResponse(ctx, requester); err != nil { + return fmt.Errorf("failed to send response to requester %s: %w", requester, err) + } + } + } + } + + return nil +} + +func (e *ServerRequest) sendResponse(ctx context.Context, requester rage.PeerID) error { + responseMsg := types.MessageBody{ + CapabilityId: e.capabilityID, + CapabilityDonId: e.capabilityDonID, + CallerDonId: e.callingDon.ID, + Method: types.MethodExecute, + MessageId: []byte(e.requestMessageID), + Sender: e.capabilityPeerID[:], + Receiver: requester[:], + CapabilityMethod: e.capMethodName, + } + + if e.response.error != types.Error_OK { + responseMsg.Error = e.response.error + responseMsg.ErrorMsg = e.response.errorMsg + } else { + responseMsg.Payload = e.response.response + } + + e.lggr.Debugw("Sending response", "receiver", requester, "capabilityId", e.capabilityID, "donId", e.capabilityDonID, "method", e.capMethodName) + err := e.dispatcher.Send(requester, &responseMsg) + e.metrics.countExecutionResponse(ctx, e.response.error.String(), err != nil) + if err != nil { + return fmt.Errorf("failed to send response to dispatcher: %w", err) + } + + e.responseSentToRequester[requester] = true + + return nil +} + +func executeCapabilityRequest(ctx context.Context, lggr logger.Logger, capability capabilities.ExecutableCapability, payload []byte, callingDonID uint32, workflowDONBindingGate limits.GateLimiter) ([]byte, error) { + capabilityRequest, err := pb.UnmarshalCapabilityRequest(payload) + if err != nil { + lggr.Errorw("failed to unmarshal capability request", "err", err) + + // Do not include the unmarshal error in the response as it may contain sensitive information + return nil, errors.New("failed to unmarshal capability request") + } + + // When enabled, bind the caller-supplied WorkflowDonID to the authenticated + // calling DON so it cannot be spoofed. All F+1 aggregated requests share this + // payload (WorkflowDonID is part of the request hash), so a single check here + // covers the quorum. The gate is guaranteed non-nil by NewServerRequest. + enabled, gerr := workflowDONBindingGate.Limit(ctx) + if gerr != nil { + lggr.Errorw("failed to evaluate workflow DON binding gate", "err", gerr) + return nil, errors.New("failed to evaluate workflow DON binding gate") + } + if enabled && capabilityRequest.Metadata.WorkflowDonID != callingDonID { + lggr.Errorw("workflow DON ID in request metadata does not match calling DON", + "metadataWorkflowDonID", capabilityRequest.Metadata.WorkflowDonID, "callingDonID", callingDonID) + return nil, fmt.Errorf("workflow DON ID %d in request metadata does not match calling DON ID %d", + capabilityRequest.Metadata.WorkflowDonID, callingDonID) + } + + lggr = logger.With(lggr, "metadata", capabilityRequest.Metadata) + + lggr.Debugw("executing capability") + capResponse, err := capability.Execute(ctx, capabilityRequest) + if err != nil { + lggr.Errorw("received execution error", "error", err) + + var capError caperrors.Error + if errors.As(err, &capError) { + return nil, errors.New(capError.SerializeToRemoteString()) + } + return nil, errors.New("failed to execute capability") + } + + responsePayload, err := pb.MarshalCapabilityResponse(capResponse) + if err != nil { + lggr.Errorw("failed to marshal capability request", "error", err) + + // Do not include the marshal error in the response as it may contain sensitive information + return nil, errors.New("failed to marshal capability request") + } + + lggr.Debug("received execution results") + return responsePayload, nil +} diff --git a/libs/x/don2don/executable/request/transmit_schedule_event.go b/libs/x/don2don/executable/request/transmit_schedule_event.go new file mode 100644 index 000000000..012300479 --- /dev/null +++ b/libs/x/don2don/executable/request/transmit_schedule_event.go @@ -0,0 +1,8 @@ +package request + +// modify with care - any changes will break downstream kafka consumers. +const ( + TransmissionEventSchema = "/workflows/v1/transmit_schedule_event.proto" + TransmissionEventProtoPkg = "workflows.v1" + TransmissionEventEntity = "TransmissionsScheduledEvent" +) diff --git a/libs/x/don2don/executable/server.go b/libs/x/don2don/executable/server.go new file mode 100644 index 000000000..f90beb2cf --- /dev/null +++ b/libs/x/don2don/executable/server.go @@ -0,0 +1,342 @@ +package executable + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + "github.com/smartcontractkit/capabilities/libs/x/don2don/executable/request" + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/don2don/validation" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// server manages all external users of a local executable capability. +// Its responsibilities are: +// 1. Manage requests from external nodes executing the executable capability once sufficient requests are received. +// 2. Send out responses produced by an underlying capability to all requesters. +// +// server communicates with corresponding client on remote nodes. +type server struct { + services.StateMachine + capabilityID string + capMethodName string + peerID rage.PeerID + dispatcher types.Dispatcher + cfg atomic.Pointer[dynamicServerConfig] + lggr logger.Logger + + requestIDToRequest map[string]requestAndMsgID + + // Used to detect messages with the same message id but different payloads + messageIDToRequestIDsCount map[string]map[string]int + + receiveLock sync.Mutex + stopCh services.StopChan + wg sync.WaitGroup + + parallelExecutor *don2don.ParallelExecutor + + // workflowDONBindingGate, when open, makes each ServerRequest require the + // request's Metadata.WorkflowDonID to match the authenticated calling DON. + workflowDONBindingGate limits.GateLimiter +} + +type dynamicServerConfig struct { + remoteExecutableConfig *commoncap.RemoteExecutableConfig + hasher types.MessageHasher + underlying commoncap.ExecutableCapability + capInfo commoncap.CapabilityInfo + localDonInfo commoncap.DON + workflowDONs map[uint32]commoncap.DON +} + +type Server interface { + types.Receiver + services.Service + SetConfig(remoteExecutableConfig *commoncap.RemoteExecutableConfig, underlying commoncap.ExecutableCapability, + capInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, workflowDONs map[uint32]commoncap.DON, + messageHasher types.MessageHasher) error +} + +var _ Server = &server{} +var _ types.Receiver = &server{} +var _ services.Service = &server{} + +type requestAndMsgID struct { + request *request.ServerRequest + messageID string +} + +func NewServer(capabilityID, methodName string, peerID rage.PeerID, dispatcher types.Dispatcher, workflowDONBindingGate limits.GateLimiter, lggr logger.Logger) *server { + return &server{ + capabilityID: capabilityID, + capMethodName: methodName, + peerID: peerID, + dispatcher: dispatcher, + lggr: logger.With(logger.Named(lggr, "ExecutableCapabilityServer"), "capabilityID", capabilityID, "capMethodName", methodName), + requestIDToRequest: map[string]requestAndMsgID{}, + messageIDToRequestIDsCount: map[string]map[string]int{}, + stopCh: make(services.StopChan), + workflowDONBindingGate: workflowDONBindingGate, + } +} + +// SetConfig sets the remote server configuration dynamically +func (r *server) SetConfig(remoteExecutableConfig *commoncap.RemoteExecutableConfig, underlying commoncap.ExecutableCapability, + capInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, workflowDONs map[uint32]commoncap.DON, messageHasher types.MessageHasher) error { + currCfg := r.cfg.Load() + if remoteExecutableConfig == nil { + r.lggr.Info("no remote config provided, using default values") + remoteExecutableConfig = &commoncap.RemoteExecutableConfig{} + } + if messageHasher == nil { + r.lggr.Warn("no message hasher provided, using default V1 hasher") + messageHasher = NewV1Hasher(remoteExecutableConfig.RequestHashExcludedAttributes) + } + if capInfo.ID == "" || capInfo.ID != r.capabilityID { + return fmt.Errorf("capability info provided does not match the server's capabilityID: %s != %s", capInfo.ID, r.capabilityID) + } + if underlying == nil { + return errors.New("underlying capability cannot be nil") + } + if len(localDonInfo.Members) == 0 { + return errors.New("empty localDonInfo provided") + } + if len(workflowDONs) == 0 { + return errors.New("empty workflowDONs provided") + } + if remoteExecutableConfig.RequestTimeout <= 0 { + return errors.New("cfg.RequestTimeout must be positive") + } + if remoteExecutableConfig.ServerMaxParallelRequests <= 0 { + return errors.New("cfg.ServerMaxParallelRequests must be positive") + } + + if currCfg != nil && currCfg.remoteExecutableConfig != nil && + currCfg.remoteExecutableConfig.ServerMaxParallelRequests > 0 && + remoteExecutableConfig.ServerMaxParallelRequests != currCfg.remoteExecutableConfig.ServerMaxParallelRequests { + r.lggr.Warn("ServerMaxParallelRequests changed but it won't be applied until node restart") + } + + // always replace the whole dynamicServerConfig object to avoid inconsistent state + r.cfg.Store(&dynamicServerConfig{ + remoteExecutableConfig: remoteExecutableConfig, + hasher: messageHasher, + underlying: underlying, + capInfo: capInfo, + localDonInfo: localDonInfo, + workflowDONs: workflowDONs, + }) + return nil +} + +func (r *server) Start(ctx context.Context) error { + return r.StartOnce(r.Name(), func() error { + cfg := r.cfg.Load() + + // Validate that all required fields are set before starting + if cfg == nil { + return errors.New("config not set - call SetConfig() before Start()") + } + if cfg.remoteExecutableConfig == nil { + return errors.New("remote executable config not set - call SetConfig() before Start()") + } + if cfg.underlying == nil { + return errors.New("underlying capability not set - call SetConfig() before Start()") + } + if cfg.capInfo.ID == "" { + return errors.New("capability info not set - call SetConfig() before Start()") + } + if len(cfg.localDonInfo.Members) == 0 { + return errors.New("local DON info not set - call SetConfig() before Start()") + } + if cfg.remoteExecutableConfig.RequestTimeout <= 0 { + return errors.New("cfg.RequestTimeout not set - call SetConfig() before Start()") + } + if cfg.remoteExecutableConfig.ServerMaxParallelRequests <= 0 { + return errors.New("cfg.ServerMaxParallelRequests not set - call SetConfig() before Start()") + } + if r.dispatcher == nil { + return errors.New("dispatcher set to nil, cannot start server") + } + + // Initialize parallel executor with the configured max parallel requests + r.parallelExecutor = don2don.NewParallelExecutor(int(cfg.remoteExecutableConfig.ServerMaxParallelRequests), "executable_server") + + r.wg.Go(func() { + ticker := time.NewTicker(getServerTickerInterval(cfg)) + defer ticker.Stop() + + r.lggr.Info("ExecutableCapabilityServer started") + for { + select { + case <-r.stopCh: + return + case <-ticker.C: + ticker.Reset(getServerTickerInterval(cfg)) + r.expireRequests() + } + } + }) + + err := r.parallelExecutor.Start(ctx) + if err != nil { + return fmt.Errorf("failed to start parallel executor: %w", err) + } + return nil + }) +} + +func getServerTickerInterval(cfg *dynamicServerConfig) time.Duration { + if cfg.remoteExecutableConfig.RequestTimeout > 0 { + return cfg.remoteExecutableConfig.RequestTimeout + } + return defaultExpiryCheckInterval +} + +func (r *server) Close() error { + return r.StopOnce(r.Name(), func() error { + close(r.stopCh) + r.wg.Wait() + if r.parallelExecutor != nil { + err := r.parallelExecutor.Close() + if err != nil { + return fmt.Errorf("failed to close parallel executor: %w", err) + } + } + + r.lggr.Info("ExecutableCapabilityServer closed") + return nil + }) +} + +func (r *server) expireRequests() { + r.receiveLock.Lock() + defer r.receiveLock.Unlock() + + for requestID, executeReq := range r.requestIDToRequest { + if executeReq.request.Expired() { + ctx, cancelFn := r.stopCh.NewCtx() + err := executeReq.request.Cancel(ctx, types.Error_TIMEOUT, "request expired by executable server") + cancelFn() + if err != nil { + r.lggr.Errorw("failed to cancel request", "request", executeReq, "err", err) + } + } + if executeReq.request.Evictable(commoncap.DefaultExecutableRequestTimeout) { + delete(r.requestIDToRequest, requestID) + delete(r.messageIDToRequestIDsCount, executeReq.messageID) + } + } +} + +func (r *server) Receive(ctx context.Context, msg *types.MessageBody) { + cfg := r.cfg.Load() + if cfg == nil { + r.lggr.Errorw("config not set, cannot process request") + return + } + + r.receiveLock.Lock() + defer r.receiveLock.Unlock() + + switch msg.Method { + case types.MethodExecute: + default: + r.lggr.Errorw("received request for unsupported method type", "method", don2don.SanitizeLogString(msg.Method)) + return + } + + messageID, err := GetMessageID(msg) + if err != nil { + r.lggr.Errorw("invalid message id", "err", err, "id", don2don.SanitizeLogString(string(msg.MessageId))) + return + } + + msgHash, err := cfg.hasher.Hash(msg) + if err != nil { + r.lggr.Errorw("failed to get message hash", "err", err) + return + } + + // A request is uniquely identified by the message id and the hash of the payload to prevent a malicious + // actor from sending a different payload with the same message id + requestID := messageID + hex.EncodeToString(msgHash[:]) + + r.lggr.Debugw("received request", "msgId", msg.MessageId, "requestID", requestID) + + if requestIDs, ok := r.messageIDToRequestIDsCount[messageID]; ok { + requestIDs[requestID]++ + } else { + r.messageIDToRequestIDsCount[messageID] = map[string]int{requestID: 1} + } + + requestIDs := r.messageIDToRequestIDsCount[messageID] + if len(requestIDs) > 1 { + // This is a potential attack vector as well as a situation that will occur if the client is sending non-deterministic payloads + // so a warning is logged + r.lggr.Warnw("received messages with the same id and different payloads", "messageID", messageID, "lenRequestIDs", len(requestIDs)) + } + + if _, ok := r.requestIDToRequest[requestID]; !ok { + callingDon, ok := cfg.workflowDONs[msg.CallerDonId] + if !ok { + r.lggr.Errorw("received request from unregistered don", "donId", msg.CallerDonId) + return + } + + sr, ierr := request.NewServerRequest(cfg.underlying, msg.Method, cfg.capInfo.ID, cfg.localDonInfo.ID, r.peerID, + callingDon, messageID, r.dispatcher, cfg.remoteExecutableConfig.RequestTimeout, r.capMethodName, r.workflowDONBindingGate, r.lggr) + if ierr != nil { + r.lggr.Errorw("failed to instantiate server request", "err", ierr) + return + } + + r.requestIDToRequest[requestID] = requestAndMsgID{ + request: sr, + messageID: messageID, + } + } + + reqAndMsgID := r.requestIDToRequest[requestID] + if executeTaskErr := r.parallelExecutor.ExecuteTask(ctx, + func(ctx context.Context) { + err = reqAndMsgID.request.OnMessage(ctx, msg) + if err != nil { + r.lggr.Errorw("failed to execute on message", "messageID", reqAndMsgID.messageID, "err", err) + } + }); executeTaskErr != nil { + r.lggr.Errorw("failed to execute on message task", "messageID", messageID, "err", executeTaskErr) + } +} + +func GetMessageID(msg *types.MessageBody) (string, error) { + idStr := string(msg.MessageId) + if !validation.IsValidID(idStr) { + return "", errors.New("invalid message id") + } + return idStr, nil +} + +func (r *server) Ready() error { + return nil +} + +func (r *server) HealthReport() map[string]error { + return nil +} + +func (r *server) Name() string { + return r.lggr.Name() +} diff --git a/libs/x/don2don/executable/server_test.go b/libs/x/don2don/executable/server_test.go new file mode 100644 index 000000000..6b3eb5278 --- /dev/null +++ b/libs/x/don2don/executable/server_test.go @@ -0,0 +1,1149 @@ +package executable_test + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/anypb" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/executable" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/internal/synctest" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +func Test_Server_Execute_SlowCapabilityExecutionDoesNotImpactSubsequentCall(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + ctx := t.Context() + + numCapabilityPeers := 4 + + workflowIDToPause := map[string]time.Duration{} + workflowIDToPause[workflowID1] = synctest.SlowDelay() + workflowIDToPause[workflowID2] = synctest.FastDelay() + + callers, srvcs := testRemoteExecutableCapabilityServer(ctx, t, &commoncap.RemoteExecutableConfig{}, &TestSlowExecutionCapability{workflowIDToPause: workflowIDToPause}, 10, 9, numCapabilityPeers, 3, 10*time.Minute, nil) + + for _, caller := range callers { + _, err := caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + _, err := caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID2, + WorkflowExecutionID: workflowExecutionID2, + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for range numCapabilityPeers { + msg := <-caller.receivedMessages + assert.Equal(t, don2dontypes.Error_OK, msg.Error) + + capabilityResponse, err := pb.UnmarshalCapabilityResponse(msg.Payload) + require.NoError(t, err) + val := capabilityResponse.Value.Underlying["response"] + + var valAsStr string + err = val.UnwrapTo(&valAsStr) + require.NoError(t, err) + + assert.Equal(t, synctest.FastDelay().String(), valAsStr) + } + } + + closeServices(t, srvcs) + }) +} + +func Test_Server_DefaultExcludedAttributes(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + numCapabilityPeers := 4 + + callers, srvcs := testRemoteExecutableCapabilityServer(ctx, t, &commoncap.RemoteExecutableConfig{}, + &TestCapability{}, 10, 9, numCapabilityPeers, 3, 10*time.Minute, nil) + + for idx, caller := range callers { + rawInputs := map[string]any{ + "StepDependency": strconv.Itoa(idx), + } + + inputs, err := values.NewMap(rawInputs) + require.NoError(t, err) + + _, err = caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + Inputs: inputs, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for range numCapabilityPeers { + msg := <-caller.receivedMessages + assert.Equal(t, don2dontypes.Error_OK, msg.Error) + } + } + closeServices(t, srvcs) +} + +func Test_Server_ExcludesNonDeterministicInputAttributes(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + numCapabilityPeers := 4 + + callers, srvcs := testRemoteExecutableCapabilityServer(ctx, t, &commoncap.RemoteExecutableConfig{RequestHashExcludedAttributes: []string{"signed_report.Signatures"}}, + &TestCapability{}, 10, 9, numCapabilityPeers, 3, 10*time.Minute, nil) + + for idx, caller := range callers { + rawInputs := map[string]any{ + "signed_report": map[string]any{"Signatures": "sig" + strconv.Itoa(idx), "Price": 20}, + } + + inputs, err := values.NewMap(rawInputs) + require.NoError(t, err) + + _, err = caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + Inputs: inputs, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for range numCapabilityPeers { + msg := <-caller.receivedMessages + assert.Equal(t, don2dontypes.Error_OK, msg.Error) + } + } + closeServices(t, srvcs) +} + +func Test_Server_Execute_RespondsAfterSufficientRequests(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + numCapabilityPeers := 4 + + callers, srvcs := testRemoteExecutableCapabilityServer(ctx, t, &commoncap.RemoteExecutableConfig{}, &TestCapability{}, 10, 9, numCapabilityPeers, 3, 10*time.Minute, nil) + + for _, caller := range callers { + _, err := caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for range numCapabilityPeers { + msg := <-caller.receivedMessages + assert.Equal(t, don2dontypes.Error_OK, msg.Error) + } + } + closeServices(t, srvcs) +} + +func Test_Server_InsufficientCallers(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + numCapabilityPeers := 4 + + callers, srvcs := testRemoteExecutableCapabilityServer(ctx, t, &commoncap.RemoteExecutableConfig{}, &TestCapability{}, 10, 10, numCapabilityPeers, 3, 100*time.Millisecond, nil) + + for _, caller := range callers { + _, err := caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for range numCapabilityPeers { + msg := <-caller.receivedMessages + assert.Equal(t, don2dontypes.Error_TIMEOUT, msg.Error) + } + } + closeServices(t, srvcs) +} + +func Test_Server_CapabilityError(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + numCapabilityPeers := 4 + + callers, srvcs := testRemoteExecutableCapabilityServer(ctx, t, &commoncap.RemoteExecutableConfig{}, &TestErrorCapability{}, 10, 9, numCapabilityPeers, 3, 10*time.Second, nil) + + for _, caller := range callers { + _, err := caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for range numCapabilityPeers { + msg := <-caller.receivedMessages + assert.Equal(t, don2dontypes.Error_INTERNAL_ERROR, msg.Error) + } + } + closeServices(t, srvcs) +} + +func Test_Server_V2Request_ExcludesNonDeterministicInputAttributes(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + numCapabilityPeers := 4 + + callers, srvcs := testRemoteExecutableCapabilityServer(ctx, t, &commoncap.RemoteExecutableConfig{RequestHashExcludedAttributes: []string{"signed_report.Signatures"}}, + &TestCapability{}, 10, 9, numCapabilityPeers, 3, 10*time.Minute, &v2WriteChainMessageHasher{}) + + report := []byte("report01234") + for idx, caller := range callers { + if idx < 0 || idx > 4294967295 { // Check bounds for uint32 + require.Fail(t, "idx out of range for uint32") + } + payload := &evm.WriteReportRequest{ + Receiver: []byte("abcdef"), + Report: &sdkpb.ReportResponse{ + RawReport: report, + Sigs: []*sdkpb.AttributedSignature{ // non-deterministic set of sigs that we want to ignore when hashing + { + SignerId: uint32(idx), // Now safe after bounds check + Signature: []byte("sig" + strconv.Itoa(idx)), + }, + }, + }, + } + anyPayload, err := anypb.New(payload) + require.NoError(t, err) + + _, err = caller.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + Payload: anyPayload, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for range numCapabilityPeers { + msg := <-caller.receivedMessages + assert.Equal(t, don2dontypes.Error_OK, msg.Error) + } + } + closeServices(t, srvcs) +} + +type v2WriteChainMessageHasher struct{} + +func (r *v2WriteChainMessageHasher) Hash(msg *don2dontypes.MessageBody) ([32]byte, error) { + req, err := pb.UnmarshalCapabilityRequest(msg.Payload) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal capability request: %w", err) + } + if req.Payload == nil { + return [32]byte{}, errors.New("request payload is nil") + } + var writeReportRequest evm.WriteReportRequest + if err = req.Payload.UnmarshalTo(&writeReportRequest); err != nil { + return [32]byte{}, fmt.Errorf("failed to unmarshal payload to WriteReportRequest: %w", err) + } + writeReportRequest.Report.Sigs = nil // exclude signatures from the hash + + req.Payload, err = anypb.New(&writeReportRequest) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal WriteReportRequest to anypb: %w", err) + } + reqBytes, err := pb.MarshalCapabilityRequest(req) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to marshal capability request: %w", err) + } + hash := sha256.Sum256(reqBytes) + return hash, nil +} + +func testRemoteExecutableCapabilityServer(ctx context.Context, t *testing.T, + config *commoncap.RemoteExecutableConfig, + underlying commoncap.ExecutableCapability, + numWorkflowPeers int, workflowDonF uint8, + numCapabilityPeers int, capabilityDonF uint8, capabilityNodeResponseTimeout time.Duration, + messageHasher don2dontypes.MessageHasher) ([]*serverTestClient, []services.Service) { + lggr := logger.Test(t) + if config.RequestTimeout == 0 { + config.RequestTimeout = capabilityNodeResponseTimeout + } + if config.ServerMaxParallelRequests == 0 { + config.ServerMaxParallelRequests = 10 + } + + capabilityPeers := make([]rage.PeerID, numCapabilityPeers) + for i := range numCapabilityPeers { + capabilityPeerID := NewP2PPeerID(t) + capabilityPeers[i] = capabilityPeerID + } + + capDonInfo := commoncap.DON{ + ID: 1, + Members: capabilityPeers, + F: capabilityDonF, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1.0.0", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Remote Target", + DON: &capDonInfo, + } + + workflowPeers := make([]rage.PeerID, numWorkflowPeers) + for i := range numWorkflowPeers { + workflowPeers[i] = NewP2PPeerID(t) + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: 2, + F: workflowDonF, + } + + srvcs := make([]services.Service, 0, 1+numCapabilityPeers) + broker := newTestAsyncMessageBroker(t, 1000) + err := broker.Start(t.Context()) + require.NoError(t, err) + srvcs = append(srvcs, broker) + + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + capabilityNodes := make([]don2dontypes.Receiver, numCapabilityPeers) + + for i := range numCapabilityPeers { + capabilityPeer := capabilityPeers[i] + capabilityDispatcher := broker.NewDispatcherForNode(capabilityPeer) + capabilityNode := executable.NewServer(capInfo.ID, "", capabilityPeer, capabilityDispatcher, limits.NewGateLimiter(false), lggr) + require.NoError(t, capabilityNode.SetConfig(config, underlying, capInfo, capDonInfo, workflowDONs, messageHasher)) + require.NoError(t, capabilityNode.Start(ctx)) + broker.RegisterReceiverNode(capabilityPeer, capabilityNode) + capabilityNodes[i] = capabilityNode + srvcs = append(srvcs, capabilityNode) + } + + workflowNodes := make([]*serverTestClient, numWorkflowPeers) + for i := range numWorkflowPeers { + workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i]) + workflowNode := newServerTestClient(workflowPeers[i], capDonInfo, workflowPeerDispatcher) + broker.RegisterReceiverNode(workflowPeers[i], workflowNode) + workflowNodes[i] = workflowNode + } + + return workflowNodes, srvcs +} + +func closeServices(t *testing.T, srvcs []services.Service) { + for _, srv := range srvcs { + require.NoError(t, srv.Close()) + } +} + +type serverTestClient struct { + peerID rage.PeerID + dispatcher don2dontypes.Dispatcher + capabilityDonInfo commoncap.DON + receivedMessages chan *don2dontypes.MessageBody + callerDonID string +} + +func (r *serverTestClient) Receive(_ context.Context, msg *don2dontypes.MessageBody) { + r.receivedMessages <- msg +} + +func newServerTestClient(peerID rage.PeerID, capabilityDonInfo commoncap.DON, + dispatcher don2dontypes.Dispatcher) *serverTestClient { + return &serverTestClient{peerID: peerID, dispatcher: dispatcher, capabilityDonInfo: capabilityDonInfo, + receivedMessages: make(chan *don2dontypes.MessageBody, 100), callerDonID: "workflow-don"} +} + +func (r *serverTestClient) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + panic("not implemented") +} + +func (r *serverTestClient) Execute(ctx context.Context, req commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + rawRequest, err := pb.MarshalCapabilityRequest(req) + if err != nil { + return nil, err + } + + messageID := don2dontypes.MethodExecute + ":" + req.Metadata.WorkflowExecutionID + + for _, node := range r.capabilityDonInfo.Members { + message := &don2dontypes.MessageBody{ + CapabilityId: "capability-id", + CapabilityDonId: 1, + CallerDonId: 2, + Method: don2dontypes.MethodExecute, + Payload: rawRequest, + MessageId: []byte(messageID), + Sender: r.peerID[:], + Receiver: node[:], + } + + if err = r.dispatcher.Send(node, message); err != nil { + return nil, err + } + } + + return nil, nil +} + +func Test_Server_SetConfig(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + peerID := NewP2PPeerID(t) + + // Create broker and dispatcher + broker := newTestAsyncMessageBroker(t, 100) + dispatcher := broker.NewDispatcherForNode(peerID) + + // Create test data + capInfo := commoncap.CapabilityInfo{ + ID: "test-capability-id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Test capability", + } + + localDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{peerID}, + F: 0, + } + + workflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []rage.PeerID{NewP2PPeerID(t)}, + F: 0, + }, + } + + underlying := &TestCapability{} + requestTimeout := 10 * time.Second + maxParallelRequests := uint32(5) + + t.Run("valid config should succeed", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + config := &commoncap.RemoteExecutableConfig{ + RequestHashExcludedAttributes: []string{"test"}, + RequestTimeout: requestTimeout, + ServerMaxParallelRequests: maxParallelRequests, + } + + err := server.SetConfig(config, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.NoError(t, err) + }) + + t.Run("mismatched capability ID should return error", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + invalidCapInfo := commoncap.CapabilityInfo{ + ID: "different-capability-id", + CapabilityType: commoncap.CapabilityTypeTarget, + } + + err := server.SetConfig(&commoncap.RemoteExecutableConfig{}, underlying, invalidCapInfo, localDonInfo, workflowDONs, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "capability info provided does not match") + }) + + t.Run("nil underlying capability should return error", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + err := server.SetConfig(&commoncap.RemoteExecutableConfig{}, nil, capInfo, + localDonInfo, workflowDONs, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "underlying capability cannot be nil") + }) + + t.Run("empty local DON members should fail", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + emptyLocalDon := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{}, + F: 0, + } + config := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 5, + } + err := server.SetConfig(config, underlying, capInfo, emptyLocalDon, workflowDONs, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "empty localDonInfo provided") + }) + + t.Run("nil message hasher should use default", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + config := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 5, + } + err := server.SetConfig(config, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.NoError(t, err) + }) + + t.Run("zero timeout should fail", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + config := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 0, + ServerMaxParallelRequests: 5, + } + err := server.SetConfig(config, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "RequestTimeout must be positive") + }) + + t.Run("zero max parallel requests should fail", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + config := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 0, + } + err := server.SetConfig(config, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "ServerMaxParallelRequests must be positive") + }) + + t.Run("empty workflow DONs should fail", func(t *testing.T) { + t.Parallel() + + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + emptyWorkflowDONs := map[uint32]commoncap.DON{} + config := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 5, + } + err := server.SetConfig(config, underlying, capInfo, localDonInfo, emptyWorkflowDONs, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "empty workflowDONs provided") + }) +} + +func Test_Server_SetConfig_ConfigReplacement(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + peerID := NewP2PPeerID(t) + broker := newTestAsyncMessageBroker(t, 100) + dispatcher := broker.NewDispatcherForNode(peerID) + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + + capInfo := commoncap.CapabilityInfo{ + ID: "test-capability-id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Test capability", + } + + localDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{peerID}, + F: 0, + } + + workflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []rage.PeerID{NewP2PPeerID(t)}, + F: 0, + }, + } + + underlying := &TestCapability{} + + // Set initial config + config1 := &commoncap.RemoteExecutableConfig{ + RequestHashExcludedAttributes: []string{"attr1"}, + RequestTimeout: 5 * time.Second, + ServerMaxParallelRequests: 3, + } + err := server.SetConfig(config1, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.NoError(t, err) + + // Verify server can start with valid config + ctx := t.Context() + err = server.Start(ctx) + require.NoError(t, err) + + // Replace with new config + config2 := &commoncap.RemoteExecutableConfig{ + RequestHashExcludedAttributes: []string{"attr2", "attr3"}, + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 5, + } + err = server.SetConfig(config2, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.NoError(t, err) + + // Clean up + err = server.Close() + require.NoError(t, err) +} + +func Test_Server_SetConfig_StartValidation(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + t.Run("Start without SetConfig should fail", func(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + peerID := NewP2PPeerID(t) + broker := newTestAsyncMessageBroker(t, 100) + dispatcher := broker.NewDispatcherForNode(peerID) + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + + err := server.Start(ctx) + require.Error(t, err) + require.Contains(t, err.Error(), "config not set - call SetConfig() before Start()") + }) + + t.Run("Start with valid config should succeed", func(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + peerID := NewP2PPeerID(t) + broker := newTestAsyncMessageBroker(t, 100) + dispatcher := broker.NewDispatcherForNode(peerID) + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + + // Set valid config + capInfo := commoncap.CapabilityInfo{ + ID: "test-capability-id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Test capability", + } + + localDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{peerID}, + F: 0, + } + + workflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []rage.PeerID{NewP2PPeerID(t)}, + F: 0, + }, + } + + underlying := &TestCapability{} + cfg := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 5, + } + err := server.SetConfig(cfg, underlying, capInfo, + localDonInfo, workflowDONs, nil) + require.NoError(t, err) + + err = server.Start(ctx) + require.NoError(t, err) + + // Clean up + err = server.Close() + require.NoError(t, err) + }) +} + +func Test_Server_SetConfig_DONMembershipChange(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + ctx := t.Context() + lggr := logger.Test(t) + peerID := NewP2PPeerID(t) + broker := newTestAsyncMessageBroker(t, 100) + dispatcher := broker.NewDispatcherForNode(peerID) + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + + capInfo := commoncap.CapabilityInfo{ + ID: "test-capability-id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Test capability", + } + + localDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{peerID}, + F: 0, + } + + workflowPeer1 := NewP2PPeerID(t) + workflowPeer2 := NewP2PPeerID(t) + workflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []rage.PeerID{workflowPeer1}, + F: 0, + }, + } + + underlying := &TestSlowExecutionCapability{ + workflowIDToPause: map[string]time.Duration{ + workflowID1: 1 * time.Second, + }, + } + + config := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 5, + } + err := server.SetConfig(config, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.NoError(t, err) + + // Set up workflow node before starting servers + workflowDispatcher := broker.NewDispatcherForNode(workflowPeer1) + workflowNode := newServerTestClient(workflowPeer1, localDonInfo, workflowDispatcher) + broker.RegisterReceiverNode(workflowPeer1, workflowNode) + broker.RegisterReceiverNode(peerID, server) + + err = server.Start(ctx) + require.NoError(t, err) + err = broker.Start(ctx) + require.NoError(t, err) + + // Start a request + _, err = workflowNode.Execute(t.Context(), commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID1, + }, + }) + require.NoError(t, err) + + // Change DON membership while request is in flight + time.Sleep(100 * time.Millisecond) + newWorkflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []rage.PeerID{workflowPeer1, workflowPeer2}, + F: 0, + }, + } + err = server.SetConfig(config, underlying, capInfo, localDonInfo, newWorkflowDONs, nil) + require.NoError(t, err) + + // Original request should still complete + select { + case msg := <-workflowNode.receivedMessages: + assert.NotNil(t, msg) + case <-time.After(5 * time.Second): + t.Fatal("request did not complete after DON change") + } + + // Clean up + require.NoError(t, server.Close()) + require.NoError(t, broker.Close()) + }) +} + +func Test_Server_SetConfig_ShutdownRaces(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + peerID := NewP2PPeerID(t) + broker := newTestAsyncMessageBroker(t, 100) + dispatcher := broker.NewDispatcherForNode(peerID) + server := executable.NewServer("test-capability-id", "test-method", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + + capInfo := commoncap.CapabilityInfo{ + ID: "test-capability-id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Test capability", + } + + localDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{peerID}, + F: 0, + } + + workflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []rage.PeerID{NewP2PPeerID(t)}, + F: 0, + }, + } + + underlying := &TestCapability{} + config := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 5, + } + + err := server.SetConfig(config, underlying, capInfo, localDonInfo, workflowDONs, nil) + require.NoError(t, err) + err = server.Start(ctx) + require.NoError(t, err) + + // Concurrently call SetConfig and Close + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + for i := range 50 { + newConfig := &commoncap.RemoteExecutableConfig{ + RequestTimeout: time.Duration(5+i) * time.Millisecond, + ServerMaxParallelRequests: 5, + } + _ = server.SetConfig(newConfig, underlying, capInfo, localDonInfo, workflowDONs, nil) + time.Sleep(1 * time.Millisecond) + } + }() + + go func() { + defer wg.Done() + time.Sleep(25 * time.Millisecond) + _ = server.Close() + }() + + wg.Wait() + // Test passes if no panic occurs +} + +func Test_Server_Execute_WithConcurrentSetConfig(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + numWorkflowPeers := 4 + + peerID := NewP2PPeerID(t) + capabilityPeers := []rage.PeerID{peerID} + + capDonInfo := commoncap.DON{ + ID: 1, + Members: capabilityPeers, + F: 0, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1.0.0", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Remote Target", + DON: &capDonInfo, + } + + workflowPeers := make([]rage.PeerID, numWorkflowPeers) + for i := range numWorkflowPeers { + workflowPeers[i] = NewP2PPeerID(t) + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: 2, + F: 1, + } + + broker := newTestAsyncMessageBroker(t, 1000) + err := broker.Start(t.Context()) + require.NoError(t, err) + defer broker.Close() + + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + // Create and set up server + dispatcher := broker.NewDispatcherForNode(peerID) + server := executable.NewServer(capInfo.ID, "", peerID, dispatcher, limits.NewGateLimiter(false), lggr) + + underlying := &TestSlowExecutionCapability{ + workflowIDToPause: map[string]time.Duration{ + workflowID1: 50 * time.Millisecond, + }, + } + + initialConfig := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 10 * time.Second, + ServerMaxParallelRequests: 10, + } + err = server.SetConfig(initialConfig, underlying, capInfo, capDonInfo, workflowDONs, nil) + require.NoError(t, err) + + err = server.Start(ctx) + require.NoError(t, err) + defer server.Close() + + broker.RegisterReceiverNode(peerID, server) + + // Create workflow nodes (callers) + workflowNodes := make([]*serverTestClient, numWorkflowPeers) + for i := range numWorkflowPeers { + workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i]) + workflowNode := newServerTestClient(workflowPeers[i], capDonInfo, workflowPeerDispatcher) + broker.RegisterReceiverNode(workflowPeers[i], workflowNode) + workflowNodes[i] = workflowNode + } + + var wg sync.WaitGroup + numExecuteCalls := 20 + numSetConfigCalls := 10 + + // Track successful responses + var successCount atomic.Int32 + var errorCount atomic.Int32 + + // Start goroutine for concurrent SetConfig calls with randomized delays + wg.Go(func() { + for i := range numSetConfigCalls { + // Random delay between 5-50ms + delay := time.Duration(5+i*2) * time.Millisecond + time.Sleep(delay) + + newConfig := &commoncap.RemoteExecutableConfig{ + RequestTimeout: time.Duration(10+i) * time.Second, + ServerMaxParallelRequests: uint32(5), + } + assert.NoError(t, server.SetConfig(newConfig, underlying, capInfo, capDonInfo, workflowDONs, nil)) + } + }) + + // Start multiple goroutines for concurrent Execute calls with randomized delays + for callerIdx, caller := range workflowNodes { + for execIdx := range numExecuteCalls { + wg.Add(1) + go func(callerID int, execID int, node *serverTestClient) { + defer wg.Done() + + // Random delay between 0-100ms + delay := time.Duration(execID*5) * time.Millisecond + time.Sleep(delay) + + workflowExecutionID := fmt.Sprintf("exec-%d", execID) + _, err := node.Execute(t.Context(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + WorkflowExecutionID: workflowExecutionID, + }, + }) + if err != nil { + t.Logf("Execute error for caller %d exec %d: %v", callerID, execID, err) + } + }(callerIdx, execIdx, caller) + } + } + + // Collect responses + expectedResponsesPerCaller := numExecuteCalls + for _, caller := range workflowNodes { + wg.Add(1) + go func(c *serverTestClient) { + defer wg.Done() + for i := range expectedResponsesPerCaller { + select { + case msg := <-c.receivedMessages: + if msg.Error == don2dontypes.Error_OK { + successCount.Add(1) + } else { + errorCount.Add(1) + } + case <-time.After(15 * time.Second): + t.Errorf("Timeout waiting for response %d/%d for a caller", i+1, expectedResponsesPerCaller) + return + } + } + }(caller) + } + + wg.Wait() + + // Verify we received responses (most should succeed) + expectedResponses := numWorkflowPeers * numExecuteCalls + require.Equal(t, expectedResponses, int(successCount.Load())) +} + +func Test_Server_DuplicateRequestRemainsDedupedPastRequestTimeout(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + + serverPeerID := NewP2PPeerID(t) + senderPeerID := NewP2PPeerID(t) + + dispatcher := &noopDispatcher{} + server := executable.NewServer("cap_id@1.0.0", "", serverPeerID, dispatcher, limits.NewGateLimiter(false), lggr) + + cfg := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 20 * time.Millisecond, + ServerMaxParallelRequests: 1, + } + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1.0.0", + CapabilityType: commoncap.CapabilityTypeTarget, + } + localDON := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{serverPeerID}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []rage.PeerID{senderPeerID}, + F: 0, + }, + } + + require.NoError(t, server.SetConfig(cfg, TestCapability{}, capInfo, localDON, workflowDONs, nil)) + require.NoError(t, server.Start(ctx)) + defer func() { + require.NoError(t, server.Close()) + }() + + inputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + require.NoError(t, err) + rawRequest, err := pb.MarshalCapabilityRequest(commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowExecutionID: "exec-1", + }, + Inputs: inputs, + }) + require.NoError(t, err) + + msg := &don2dontypes.MessageBody{ + CapabilityId: capInfo.ID, + CapabilityDonId: localDON.ID, + CallerDonId: 2, + Method: don2dontypes.MethodExecute, + Payload: rawRequest, + MessageId: []byte(don2dontypes.MethodExecute + ":exec-1"), + Sender: senderPeerID[:], + Receiver: serverPeerID[:], + } + + server.Receive(ctx, msg) + require.Eventually(t, func() bool { return dispatcher.Len() == 1 }, time.Second, 10*time.Millisecond) + + time.Sleep(2 * cfg.RequestTimeout) + server.Receive(ctx, msg) + + require.Never(t, func() bool { return dispatcher.Len() > 1 }, 100*time.Millisecond, 10*time.Millisecond) +} + +type noopDispatcher struct { + services.StateMachine + mu sync.Mutex + sent []*don2dontypes.MessageBody +} + +func (n *noopDispatcher) Name() string { return "noopDispatcher" } + +func (n *noopDispatcher) Start(context.Context) error { return nil } + +func (n *noopDispatcher) Close() error { return nil } + +func (n *noopDispatcher) Ready() error { return nil } + +func (n *noopDispatcher) HealthReport() map[string]error { return nil } + +func (n *noopDispatcher) SetReceiver(string, uint32, don2dontypes.Receiver) error { return nil } + +func (n *noopDispatcher) RemoveReceiver(string, uint32) {} + +func (n *noopDispatcher) SetReceiverForMethod(string, uint32, string, don2dontypes.Receiver) error { + return nil +} + +func (n *noopDispatcher) RemoveReceiverForMethod(string, uint32, string) {} + +func (n *noopDispatcher) Send(peerID rage.PeerID, msgBody *don2dontypes.MessageBody) error { + n.mu.Lock() + defer n.mu.Unlock() + n.sent = append(n.sent, msgBody) + return nil +} + +func (n *noopDispatcher) Len() int { + n.mu.Lock() + defer n.mu.Unlock() + return len(n.sent) +} diff --git a/libs/x/don2don/messagecache/message_cache.go b/libs/x/don2don/messagecache/message_cache.go new file mode 100644 index 000000000..d5e7081d8 --- /dev/null +++ b/libs/x/don2don/messagecache/message_cache.go @@ -0,0 +1,111 @@ +package messagecache + +// MessageCache is a simple store for messages, grouped by event ID and peer ID. +// It is used to collect messages from multiple peers until they are ready for aggregation +// based on quantity and freshness. +type MessageCache[EventID comparable, PeerID comparable] struct { + events map[EventID]*eventState[PeerID] +} + +type eventState[PeerID comparable] struct { + peerMsgs map[PeerID]*msgState + creationTimestamp int64 + wasReady bool +} + +type msgState struct { + timestamp int64 + payload []byte +} + +func NewMessageCache[EventID comparable, PeerID comparable]() *MessageCache[EventID, PeerID] { + return &MessageCache[EventID, PeerID]{ + events: make(map[EventID]*eventState[PeerID]), + } +} + +// Insert or overwrite a message for . Return creation timestamp of the event. +func (c *MessageCache[EventID, PeerID]) Insert(eventID EventID, peerID PeerID, timestamp int64, payload []byte) int64 { + if _, ok := c.events[eventID]; !ok { + c.events[eventID] = &eventState[PeerID]{ + peerMsgs: make(map[PeerID]*msgState), + creationTimestamp: timestamp, + } + } + c.events[eventID].peerMsgs[peerID] = &msgState{ + timestamp: timestamp, + payload: payload, + } + return c.events[eventID].creationTimestamp +} + +// Return true if there are messages from at least peers, +// received more recently than . +// Return all messages that satisfy the above condition. +// Ready() will return true at most once per event if is true. +func (c *MessageCache[EventID, PeerID]) Ready(eventID EventID, minCount uint32, minTimestamp int64, once bool) (bool, [][]byte) { + ev, ok := c.events[eventID] + if !ok { + return false, nil + } + if ev.wasReady && once { + return false, nil + } + //nolint:gosec // G115 + if uint32(len(ev.peerMsgs)) < minCount { + return false, nil + } + countAboveMinTimestamp := uint32(0) + accPayloads := [][]byte{} + for _, msg := range ev.peerMsgs { + if msg.timestamp >= minTimestamp { + countAboveMinTimestamp++ + accPayloads = append(accPayloads, msg.payload) + } + } + if countAboveMinTimestamp >= minCount { + ev.wasReady = true + return true, accPayloads + } + return false, nil +} + +// WasReady reports whether Ready has already returned true for eventID (once=true path). +func (c *MessageCache[EventID, PeerID]) WasReady(eventID EventID) bool { + ev, ok := c.events[eventID] + if !ok { + return false + } + return ev.wasReady +} + +func (c *MessageCache[EventID, PeerID]) Delete(eventID EventID) { + delete(c.events, eventID) +} + +// Peers returns a snapshot of peer IDs that have inserted a message for eventID. +func (c *MessageCache[EventID, PeerID]) Peers(eventID EventID) map[PeerID]bool { + ev, ok := c.events[eventID] + if !ok { + return nil + } + + peers := make(map[PeerID]bool, len(ev.peerMsgs)) + for peerID := range ev.peerMsgs { + peers[peerID] = true + } + return peers +} + +// Return the number of events deleted. +// Scans all keys, which might be slow for large caches. +func (c *MessageCache[EventID, PeerID]) DeleteOlderThan(cutoffTimestamp int64) int { + nDeleted := 0 + for id, event := range c.events { + if event.creationTimestamp < cutoffTimestamp { + c.Delete(id) + nDeleted++ + } + } + return nDeleted +} diff --git a/libs/x/don2don/messagecache/message_cache_test.go b/libs/x/don2don/messagecache/message_cache_test.go new file mode 100644 index 000000000..9afdfd039 --- /dev/null +++ b/libs/x/don2don/messagecache/message_cache_test.go @@ -0,0 +1,66 @@ +package messagecache_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/messagecache" +) + +const ( + eventID1 = "event1" + eventID2 = "event2" + peerID1 = "peer1" + peerID2 = "peer2" + payloadA = "payloadA" +) + +func TestMessageCache_InsertReady(t *testing.T) { + t.Parallel() + + cache := messagecache.NewMessageCache[string, string]() + + // not ready with one message + ts := cache.Insert(eventID1, peerID1, 100, []byte(payloadA)) + require.Equal(t, int64(100), ts) + ready, _ := cache.Ready(eventID1, 2, 100, true) + require.False(t, ready) + + // not ready with two messages but only one fresh enough + ts = cache.Insert(eventID1, peerID2, 200, []byte(payloadA)) + require.Equal(t, int64(100), ts) + ready, _ = cache.Ready(eventID1, 2, 150, true) + require.False(t, ready) + + // ready with two messages (once only) + ready, messages := cache.Ready(eventID1, 2, 100, true) + require.True(t, ready) + require.Equal(t, []byte(payloadA), messages[0]) + require.Equal(t, []byte(payloadA), messages[1]) + + // not ready again for the same event ID + ready, _ = cache.Ready(eventID1, 2, 100, true) + require.False(t, ready) + require.True(t, cache.WasReady(eventID1)) +} + +func TestMessageCache_DeleteOlderThan(t *testing.T) { + t.Parallel() + + cache := messagecache.NewMessageCache[string, string]() + + ts := cache.Insert(eventID1, peerID1, 100, []byte(payloadA)) + require.Equal(t, int64(100), ts) + ts = cache.Insert(eventID2, peerID2, 200, []byte(payloadA)) + require.Equal(t, int64(200), ts) + + deleted := cache.DeleteOlderThan(150) + require.Equal(t, 1, deleted) + + deleted = cache.DeleteOlderThan(150) + require.Equal(t, 0, deleted) + + deleted = cache.DeleteOlderThan(201) + require.Equal(t, 1, deleted) +} diff --git a/libs/x/don2don/parallel_executor.go b/libs/x/don2don/parallel_executor.go new file mode 100644 index 000000000..57b0575a8 --- /dev/null +++ b/libs/x/don2don/parallel_executor.go @@ -0,0 +1,122 @@ +package don2don + +import ( + "context" + "errors" + "fmt" + "sync" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// ParallelExecutor runs tasks concurrently up to a configured limit. +type ParallelExecutor struct { + services.StateMachine + wg sync.WaitGroup + stopChan services.StopChan + + taskSemaphore chan struct{} + name string + slotUsageGauge metric.Float64Gauge + slotUsageAttrs []attribute.KeyValue +} + +// NewParallelExecutor creates an executor that allows at most maxParallelTasks in-flight tasks. +func NewParallelExecutor(maxParallelTasks int, name string, attrs ...attribute.KeyValue) *ParallelExecutor { + attrs = append(attrs, attribute.String("parallel_executor_name", name)) + e := &ParallelExecutor{ + stopChan: make(services.StopChan), + wg: sync.WaitGroup{}, + taskSemaphore: make(chan struct{}, maxParallelTasks), + name: name, + slotUsageAttrs: attrs, + } + return e +} + +// recordSlotUsage publishes current slot utilization to the configured gauge, if any. +func (t *ParallelExecutor) recordSlotUsage(ctx context.Context) { + if t.slotUsageGauge == nil { + return + } + maxSlots := t.maxSlots() + if maxSlots == 0 { + return + } + usage := float64(t.occupiedSlots()) / float64(maxSlots) + t.slotUsageGauge.Record(ctx, usage, metric.WithAttributes(t.slotUsageAttrs...)) +} + +// occupiedSlots returns the number of in-flight tasks holding executor slots. +func (t *ParallelExecutor) occupiedSlots() int { + return len(t.taskSemaphore) +} + +// maxSlots returns the maximum number of concurrent tasks allowed. +func (t *ParallelExecutor) maxSlots() int { + return cap(t.taskSemaphore) +} + +// ExecuteTask executes a task in parallel up to the maximum allowed parallel executions. If the +// maximum execute limit is reached, the function will block until a slot is available or the +// context is cancelled. +func (t *ParallelExecutor) ExecuteTask(ctx context.Context, fn func(ctx context.Context)) error { + select { + case t.taskSemaphore <- struct{}{}: + t.recordSlotUsage(ctx) + stopped := !t.IfNotStopped(func() { + t.wg.Go(func() { + ctxWithStop, cancel := t.stopChan.Ctx(ctx) + defer func() { + <-t.taskSemaphore + t.recordSlotUsage(ctxWithStop) + cancel() + }() + fn(ctxWithStop) + }) + }) + + if stopped { + return errors.New("executor stopped") + } + case <-t.stopChan: + return nil + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} + +// Start starts the executor. +func (t *ParallelExecutor) Start(ctx context.Context) error { + var err error + t.slotUsageGauge, err = beholder.GetMeter().Float64Gauge("platform_parallel_executor_slot_usage") + if err != nil { + return fmt.Errorf("failed to register platform_parallel_executor_slot_usage: %w", err) + } + return t.StartOnce(t.Name(), func() error { + return nil + }) +} + +// Close stops the executor and waits for in-flight tasks to finish. +func (t *ParallelExecutor) Close() error { + return t.StopOnce(t.Name(), func() error { + close(t.stopChan) + t.wg.Wait() + return nil + }) +} + +// Name returns the service name. +func (t *ParallelExecutor) Name() string { + if t.name != "" { + return t.name + } + return "ParallelExecutor" +} diff --git a/libs/x/don2don/parallel_executor_test.go b/libs/x/don2don/parallel_executor_test.go new file mode 100644 index 000000000..2dac92a00 --- /dev/null +++ b/libs/x/don2don/parallel_executor_test.go @@ -0,0 +1,105 @@ +package don2don_test + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" +) + +func Test_CancellingContext_StopsTask(t *testing.T) { + t.Parallel() + tp := don2don.NewParallelExecutor(10, "test_parallel_executor") + servicetest.Run(t, tp) + + cancelFns := make([]context.CancelFunc, 0, 10) + + var counter atomic.Int32 + for range 10 { + ctx, cancel := context.WithCancel(t.Context()) + t.Cleanup(cancel) + cancelFns = append(cancelFns, cancel) + err := tp.ExecuteTask(ctx, func(ctx context.Context) { + counter.Add(1) + <-ctx.Done() + counter.Add(-1) + }) + + require.NoError(t, err) + } + + assert.Eventually(t, func() bool { return counter.Load() == 10 }, 5*time.Second, 10*time.Millisecond) + + for _, cancel := range cancelFns { + cancel() + } + + assert.Eventually(t, func() bool { return counter.Load() == 0 }, 5*time.Second, 10*time.Millisecond) +} + +func Test_ExecuteRequestTimesOutWhenParallelExecutionLimitReached(t *testing.T) { + t.Parallel() + tp := don2don.NewParallelExecutor(3, "test_parallel_executor") + servicetest.Run(t, tp) + + for range 3 { + err := tp.ExecuteTask(t.Context(), func(ctx context.Context) { + <-ctx.Done() + }) + require.NoError(t, err) + } + + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + err := tp.ExecuteTask(ctx, func(ctx context.Context) { + }) + cancel() + require.Error(t, err) + require.ErrorIs(t, err, context.DeadlineExceeded) +} + +func Test_ExecutingMultipleTasksInParallel(t *testing.T) { + t.Parallel() + tp := don2don.NewParallelExecutor(10, "test_parallel_executor") + servicetest.Run(t, tp) + + var counter atomic.Int32 + for range 10 { + err := tp.ExecuteTask(t.Context(), func(ctx context.Context) { + counter.Add(1) + <-ctx.Done() + counter.Add(-1) + }) + require.NoError(t, err) + } + + assert.Eventually(t, func() bool { return counter.Load() == 10 }, 5*time.Second, 10*time.Millisecond) +} + +func Test_StopsExecutingMultipleParallelTasksWhenClosed(t *testing.T) { + t.Parallel() + tp := don2don.NewParallelExecutor(10, "test_parallel_executor") + var counter atomic.Int32 + t.Cleanup(func() { + assert.Eventually(t, func() bool { return counter.Load() == 0 }, 5*time.Second, 10*time.Millisecond) + }) + + servicetest.Run(t, tp) + + for range 10 { + err := tp.ExecuteTask(t.Context(), func(ctx context.Context) { + counter.Add(1) + <-ctx.Done() + counter.Add(-1) + }) + require.NoError(t, err) + } + + assert.Eventually(t, func() bool { return counter.Load() == 10 }, 5*time.Second, 10*time.Millisecond) +} diff --git a/libs/x/don2don/transmission/local_executable_capability.go b/libs/x/don2don/transmission/local_executable_capability.go new file mode 100644 index 000000000..03a2003f8 --- /dev/null +++ b/libs/x/don2don/transmission/local_executable_capability.go @@ -0,0 +1,45 @@ +package transmission + +import ( + "context" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// LocalExecutableCapability handles the transmission protocol required for a target capability that exists in the same don as +// the caller. +type LocalExecutableCapability struct { + lggr logger.Logger + capabilities.ExecutableCapability + localNode capabilities.Node + capabilityID string +} + +func NewLocalExecutableCapability(lggr logger.Logger, capabilityID string, localDON capabilities.Node, underlying capabilities.ExecutableCapability) *LocalExecutableCapability { + return &LocalExecutableCapability{ + ExecutableCapability: underlying, + capabilityID: capabilityID, + lggr: lggr, + localNode: localDON, + } +} + +func (l *LocalExecutableCapability) Execute(ctx context.Context, req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + if l.localNode.PeerID == nil || l.localNode.WorkflowDON.ID == 0 { + l.lggr.Debug("empty DON info, executing immediately") + return l.ExecutableCapability.Execute(ctx, req) + } + + response, err := l.ExecutableCapability.Execute(ctx, req) + if err != nil { + return response, err + } + + // Set peer2peerID in the response metadata for local capabilities + if len(response.Metadata.Metering) == 1 { + response.Metadata.Metering[0].Peer2PeerID = l.localNode.PeerID.String() + } + + return response, nil +} diff --git a/libs/x/don2don/transmission/local_target_capability.go b/libs/x/don2don/transmission/local_target_capability.go new file mode 100644 index 000000000..c3c7414fc --- /dev/null +++ b/libs/x/don2don/transmission/local_target_capability.go @@ -0,0 +1,57 @@ +package transmission + +import ( + "context" + "fmt" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// LocalTargetCapability handles the transmission protocol required for a target capability that exists in the same don as +// the caller. +type LocalTargetCapability struct { + lggr logger.Logger + capabilities.TargetCapability + localNode capabilities.Node + capabilityID string +} + +func NewLocalTargetCapability(lggr logger.Logger, capabilityID string, localDON capabilities.Node, underlying capabilities.TargetCapability) *LocalTargetCapability { + return &LocalTargetCapability{ + TargetCapability: underlying, + capabilityID: capabilityID, + lggr: lggr, + localNode: localDON, + } +} + +func (l *LocalTargetCapability) Execute(ctx context.Context, req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + if l.localNode.PeerID == nil || l.localNode.WorkflowDON.ID == 0 { + l.lggr.Debug("empty DON info, executing immediately") + return l.TargetCapability.Execute(ctx, req) + } + + if req.Config == nil || req.Config.Underlying["schedule"] == nil { + l.lggr.Debug("no schedule found, executing immediately") + return l.TargetCapability.Execute(ctx, req) + } + + peerIDToTransmissionDelay, err := GetPeerIDToTransmissionDelay(l.localNode.WorkflowDON.Members, req) + if err != nil { + return capabilities.CapabilityResponse{}, fmt.Errorf("capability id: %s failed to get peer ID to transmission delay map: %w", l.capabilityID, err) + } + + delay, existsForPeerID := peerIDToTransmissionDelay[*l.localNode.PeerID] + if !existsForPeerID { + return capabilities.CapabilityResponse{}, nil + } + + select { + case <-ctx.Done(): + return capabilities.CapabilityResponse{}, ctx.Err() + case <-time.After(delay): + return l.TargetCapability.Execute(ctx, req) + } +} diff --git a/libs/x/don2don/transmission/local_target_capability_test.go b/libs/x/don2don/transmission/local_target_capability_test.go new file mode 100644 index 000000000..30bc5736f --- /dev/null +++ b/libs/x/don2don/transmission/local_target_capability_test.go @@ -0,0 +1,203 @@ +package transmission + +import ( + "context" + "crypto/rand" + "testing" + "testing/synctest" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +func assertBetween(t *testing.T, got time.Duration, low time.Duration, high time.Duration) { + assert.GreaterOrEqual(t, got, low) + assert.LessOrEqual(t, got, high) +} + +func TestScheduledExecutionStrategy_LocalDON(t *testing.T) { + var gotTime time.Time + var called bool + + log := logger.Test(t) + + // Our capability has DONInfo == nil, so we'll treat it as a local + // capability and use the local DON Info to determine the transmission + // schedule. + mt := newMockCapability( + capabilities.MustNewCapabilityInfo( + "write_polygon-testnet-mumbai@1.0.0", + capabilities.CapabilityTypeTarget, + "a write capability targeting polygon mumbai testnet", + ), + func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + gotTime = time.Now() + called = true + return capabilities.CapabilityResponse{}, nil + }, + ) + + testCases := []struct { + name string + position int + schedule string + low time.Duration + high time.Duration + }{ + { + name: "position 0; oneAtATime", + position: 0, + schedule: "oneAtATime", + low: 200 * time.Millisecond, + high: 300 * time.Millisecond, + }, + { + name: "position 1; oneAtATime", + position: 1, + schedule: "oneAtATime", + low: 300 * time.Millisecond, + high: 400 * time.Millisecond, + }, + { + name: "position 2; oneAtATime", + position: 2, + schedule: "oneAtATime", + low: 100 * time.Millisecond, + high: 200 * time.Millisecond, + }, + { + name: "position 3; oneAtATime", + position: 3, + schedule: "oneAtATime", + low: 0 * time.Millisecond, + high: 100 * time.Millisecond, + }, + { + name: "position 0; allAtOnce", + position: 0, + schedule: "allAtOnce", + low: 0 * time.Millisecond, + high: 100 * time.Millisecond, + }, + { + name: "position 1; allAtOnce", + position: 1, + schedule: "allAtOnce", + low: 0 * time.Millisecond, + high: 100 * time.Millisecond, + }, + { + name: "position 2; allAtOnce", + position: 2, + schedule: "allAtOnce", + low: 0 * time.Millisecond, + high: 100 * time.Millisecond, + }, + { + name: "position 3; allAtOnce", + position: 3, + schedule: "allAtOnce", + low: 0 * time.Millisecond, + high: 100 * time.Millisecond, + }, + } + + for _, tc := range testCases { + synctestrun(t, tc.name, func(t *testing.T) { + startTime := time.Now() + + m, err := values.NewMap(map[string]any{ + "schedule": tc.schedule, + "deltaStage": "100ms", + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Config: m, + Metadata: capabilities.RequestMetadata{ + WorkflowID: "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0", + WorkflowExecutionID: "32c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce1", + }, + } + + ids := []rage.PeerID{ + randKey(), + randKey(), + randKey(), + randKey(), + } + localDON := capabilities.Node{ + WorkflowDON: capabilities.DON{ + ID: 1, + Members: ids, + }, + PeerID: &ids[tc.position], + } + localTargetCapability := NewLocalTargetCapability(log, "capabilityID", localDON, mt) + + _, err = localTargetCapability.Execute(t.Context(), req) + + require.NoError(t, err) + require.True(t, called) + + assertBetween(t, gotTime.Sub(startTime), tc.low, tc.high) + }) + } +} + +func randKey() [32]byte { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + return [32]byte(key) +} + +type mockCapability struct { + capabilities.CapabilityInfo + capabilities.Executable + response chan capabilities.CapabilityResponse + transform func(capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) +} + +func newMockCapability(info capabilities.CapabilityInfo, transform func(capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error)) *mockCapability { + return &mockCapability{ + transform: transform, + CapabilityInfo: info, + response: make(chan capabilities.CapabilityResponse, 10), + } +} + +func (m *mockCapability) Execute(ctx context.Context, req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + cr, err := m.transform(req) + if err != nil { + return capabilities.CapabilityResponse{}, err + } + + m.response <- cr + return cr, nil +} + +func (m *mockCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (m *mockCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} + +func synctestrun(t *testing.T, name string, f func(*testing.T)) { + t.Helper() + t.Run(name, func(t *testing.T) { + t.Helper() + synctest.Test(t, f) + }) +} diff --git a/libs/x/don2don/transmission/transmission.go b/libs/x/don2don/transmission/transmission.go new file mode 100644 index 000000000..7c1b40836 --- /dev/null +++ b/libs/x/don2don/transmission/transmission.go @@ -0,0 +1,144 @@ +package transmission + +import ( + "fmt" + "time" + + "github.com/smartcontractkit/libocr/permutation" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/validation" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + "github.com/smartcontractkit/capabilities/libs/x/rage" + + "golang.org/x/crypto/sha3" +) + +var ( + // S = [N] + Schedule_AllAtOnce = "allAtOnce" + // S = [1 * N] + Schedule_OneAtATime = "oneAtATime" +) + +type TransmissionConfig struct { + Schedule string + DeltaStage time.Duration +} + +func (tc *TransmissionConfig) String() string { + return fmt.Sprintf("[Schedule: %s, DeltaStage: %s]", tc.Schedule, tc.DeltaStage) +} + +func ExtractTransmissionConfig(config *values.Map) (TransmissionConfig, error) { + var tc struct { + DeltaStage string + Schedule string + } + err := config.UnwrapTo(&tc) + if err != nil { + return TransmissionConfig{}, fmt.Errorf("failed to unwrap tranmission config from value map: %w", err) + } + + // Default if no schedule and deltaStage is provided + if len(tc.Schedule) == 0 && len(tc.DeltaStage) == 0 { + return TransmissionConfig{ + Schedule: Schedule_AllAtOnce, + DeltaStage: 0, + }, nil + } + + duration, err := time.ParseDuration(tc.DeltaStage) + if err != nil { + return TransmissionConfig{}, fmt.Errorf("failed to parse DeltaStage %s as duration: %w", tc.DeltaStage, err) + } + + return TransmissionConfig{ + Schedule: tc.Schedule, + DeltaStage: duration, + }, nil +} + +func EnumToString(t capabilities.TransmissionSchedule) string { + switch t { + case capabilities.Schedule_AllAtOnce: + return Schedule_AllAtOnce + case capabilities.Schedule_OneAtATime: + return Schedule_OneAtATime + default: + return "unknown" + } +} + +// GetPeerIDToTransmissionDelay returns a map of PeerID to the time.Duration that the node with that PeerID should wait +// before transmitting the capability request. If a node is not in the map, it should not transmit. +func GetPeerIDToTransmissionDelay(donPeerIDs []rage.PeerID, req capabilities.CapabilityRequest) (map[rage.PeerID]time.Duration, error) { + tc, err := ExtractTransmissionConfig(req.Config) + if err != nil { + return nil, fmt.Errorf("failed to extract transmission config from request: %w", err) + } + + workflowExecutionID := req.Metadata.WorkflowExecutionID + if err := validation.ValidateWorkflowOrExecutionID(workflowExecutionID); err != nil { + return nil, fmt.Errorf("workflow or execution ID is invalid: %w", err) + } + + return GetPeerIDToTransmissionDelaysForConfig(donPeerIDs, workflowExecutionID, tc) +} + +func GetPeerIDToTransmissionDelaysForConfig(donPeerIDs []rage.PeerID, transmissionID string, tc TransmissionConfig) (map[rage.PeerID]time.Duration, error) { + donMemberCount := len(donPeerIDs) + key := transmissionScheduleSeed(transmissionID) + schedule, err := createTransmissionSchedule(tc.Schedule, donMemberCount) + if err != nil { + return nil, err + } + + picked := permutation.Permutation(donMemberCount, key) + + peerIDToTransmissionDelay := map[rage.PeerID]time.Duration{} + for i, peerID := range donPeerIDs { + delay := delayFor(i, schedule, picked, tc.DeltaStage) + if delay != nil { + peerIDToTransmissionDelay[peerID] = *delay + } + } + return peerIDToTransmissionDelay, nil +} + +func delayFor(position int, schedule []int, permutation []int, deltaStage time.Duration) *time.Duration { + sum := 0 + for i, s := range schedule { + sum += s + if permutation[position] < sum { + result := time.Duration(i) * deltaStage + return &result + } + } + + return nil +} + +func createTransmissionSchedule(scheduleType string, N int) ([]int, error) { + switch scheduleType { + case Schedule_AllAtOnce: + return []int{N}, nil + case Schedule_OneAtATime: + sch := []int{} + for range N { + sch = append(sch, 1) + } + return sch, nil + } + return nil, fmt.Errorf("unknown schedule type %s", scheduleType) +} + +func transmissionScheduleSeed(transmissionID string) [16]byte { + hash := sha3.NewLegacyKeccak256() + hash.Write([]byte(transmissionID)) + var key [16]byte + copy(key[:], hash.Sum(nil)) + return key +} diff --git a/libs/x/don2don/transmission/transmission_test.go b/libs/x/don2don/transmission/transmission_test.go new file mode 100644 index 000000000..3d57ffc87 --- /dev/null +++ b/libs/x/don2don/transmission/transmission_test.go @@ -0,0 +1,103 @@ +package transmission + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +func Test_GetPeerIDToTransmissionDelay(t *testing.T) { + peer1 := [32]byte(fmt.Appendf(nil, "%-32s", "one")) + peer2 := [32]byte(fmt.Appendf(nil, "%-32s", "two")) + peer3 := [32]byte(fmt.Appendf(nil, "%-32s", "three")) + peer4 := [32]byte(fmt.Appendf(nil, "%-32s", "four")) + + ids := []rage.PeerID{ + peer1, peer2, peer3, peer4, + } + + testCases := []struct { + name string + peerName string + schedule string + deltaStage string + workflowExecutionID string + expectedDelays map[string]time.Duration + }{ + { + "TestOneAtATime", + "one", + "oneAtATime", + "100ms", + "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0", + map[string]time.Duration{ + "one": 100 * time.Millisecond, + "two": 200 * time.Millisecond, + "three": 300 * time.Millisecond, + "four": 0 * time.Millisecond, + }, + }, + + { + "TestAllAtOnce", + "one", + "allAtOnce", + "100ms", + "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0", + map[string]time.Duration{ + "one": 0 * time.Millisecond, + "two": 0 * time.Millisecond, + "three": 0 * time.Millisecond, + "four": 0 * time.Millisecond, + }, + }, + + { + "TestOneAtATimeWithDifferentExecutionID", + "one", + "oneAtATime", + "100ms", + "16c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce1", + map[string]time.Duration{ + "one": 200 * time.Millisecond, + "two": 100 * time.Millisecond, + "three": 0 * time.Millisecond, + "four": 300 * time.Millisecond, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + transmissionCfg, err := values.NewMap(map[string]any{ + "schedule": tc.schedule, + "deltaStage": tc.deltaStage, + }) + require.NoError(t, err) + + capabilityRequest := capabilities.CapabilityRequest{ + Config: transmissionCfg, + Metadata: capabilities.RequestMetadata{ + WorkflowID: "17c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0", + WorkflowExecutionID: tc.workflowExecutionID, + }, + } + + peerIdToDelay, err := GetPeerIDToTransmissionDelay(ids, capabilityRequest) + require.NoError(t, err) + + assert.Equal(t, tc.expectedDelays["one"], peerIdToDelay[peer1]) + assert.Equal(t, tc.expectedDelays["two"], peerIdToDelay[peer2]) + assert.Equal(t, tc.expectedDelays["three"], peerIdToDelay[peer3]) + assert.Equal(t, tc.expectedDelays["four"], peerIdToDelay[peer4]) + }) + } +} diff --git a/libs/x/don2don/trigger_publisher.go b/libs/x/don2don/trigger_publisher.go new file mode 100644 index 000000000..d5d1ff1dd --- /dev/null +++ b/libs/x/don2don/trigger_publisher.go @@ -0,0 +1,950 @@ +package don2don + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "strconv" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/aggregation" + "github.com/smartcontractkit/capabilities/libs/x/don2don/messagecache" + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/don2don/validation" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// TriggerPublisher manages all external users of a local trigger capability. +// Its responsibilities are: +// 1. Manage trigger registrations from external nodes (receive, store, aggregate, expire). +// 2. Send out events produced by an underlying, concrete trigger implementation. +// +// TriggerPublisher communicates with corresponding TriggerSubscribers on remote nodes. +type triggerPublisher struct { + capabilityID string + capMethodName string + dispatcher types.Dispatcher + cfg atomic.Pointer[dynamicPublisherConfig] + + messageCache *messagecache.MessageCache[registrationKey, rage.PeerID] + registrations map[registrationKey]*pubRegState + unregisterCache *messagecache.MessageCache[registrationKey, rage.PeerID] + ackCache *messagecache.MessageCache[ackKey, rage.PeerID] + mu sync.RWMutex // protects messageCache, ackCache, unregisterCache, and registrations + batchingQueue map[[32]byte]*batchedResponse + bqMu sync.Mutex // protects batchingQueue + stopCh services.StopChan + wg sync.WaitGroup + ackExecutor *ParallelExecutor + registerExecutor *ParallelExecutor + lggr logger.Logger + metrics triggerPublisherMetrics +} + +type triggerPublisherMetrics struct { + registerTriggerCounter metric.Int64Counter + unregisterTriggerCounter metric.Int64Counter + ackEventCounter metric.Int64Counter + registerTriggerDurationMs metric.Int64Histogram + unregisterTriggerDurationMs metric.Int64Histogram + ackEventDurationMs metric.Int64Histogram +} + +type dynamicPublisherConfig struct { + remoteConfig *commoncap.RemoteTriggerConfig + underlying commoncap.TriggerCapability + capDonInfo commoncap.DON + workflowDONs map[uint32]commoncap.DON + membersCache map[uint32]map[rage.PeerID]bool + batchingEnabled bool +} + +type registrationKey struct { + callerDonID uint32 + workflowID string + triggerID string +} + +type ackKey struct { + callerDonID uint32 + triggerEventID string + triggerID string // triggerID contains the workflowID +} + +type pubRegState struct { + callback <-chan commoncap.TriggerResponse + request commoncap.TriggerRegistrationRequest + cancel context.CancelFunc + registrationErr error // non-nil if RegisterTrigger returned an error; used to suppress retries +} + +type batchedResponse struct { + rawResponse []byte + callerDonID uint32 + triggerEventID string + workflowIDs []string + triggerIDs []string +} + +type TriggerPublisher interface { + types.ReceiverService + SetConfig(config *commoncap.RemoteTriggerConfig, underlying commoncap.TriggerCapability, capDonInfo commoncap.DON, workflowDONs map[uint32]commoncap.DON) error +} + +var _ TriggerPublisher = &triggerPublisher{} +var _ types.ReceiverService = &triggerPublisher{} + +const ( + minAllowedBatchCollectionPeriod = 10 * time.Millisecond + defaultMaxParallelAcks = 100 // TODO: make this configurable https://smartcontract-it.atlassian.net/browse/PLEX-3266 + defaultMaxParallelRegisters = 100 // TODO: make this configurable https://smartcontract-it.atlassian.net/browse/PLEX-3266 +) + +func NewTriggerPublisher(capabilityID string, capMethodName string, dispatcher types.Dispatcher, lggr logger.Logger) *triggerPublisher { + slotUsageAttrs := []attribute.KeyValue{ + attribute.String("capabilityID", capabilityID), + attribute.String("capMethodName", capMethodName), + } + + return &triggerPublisher{ + capabilityID: capabilityID, + capMethodName: capMethodName, + dispatcher: dispatcher, + messageCache: messagecache.NewMessageCache[registrationKey, rage.PeerID](), + ackCache: messagecache.NewMessageCache[ackKey, rage.PeerID](), + unregisterCache: messagecache.NewMessageCache[registrationKey, rage.PeerID](), + registrations: make(map[registrationKey]*pubRegState), + batchingQueue: make(map[[32]byte]*batchedResponse), + stopCh: make(services.StopChan), + lggr: logger.With(logger.Named(lggr, "TriggerPublisher"), "capabilityID", capabilityID, "capMethodName", capMethodName), + ackExecutor: NewParallelExecutor(defaultMaxParallelAcks, + "trigger_publisher_ack_executor", + slotUsageAttrs..., + ), + registerExecutor: NewParallelExecutor(defaultMaxParallelRegisters, + "trigger_publisher_register_executor", + slotUsageAttrs..., + ), + } +} + +// SetConfig sets the remote trigger configuration, capability info, and DON information dynamically +func (p *triggerPublisher) SetConfig(config *commoncap.RemoteTriggerConfig, underlying commoncap.TriggerCapability, capDonInfo commoncap.DON, workflowDONs map[uint32]commoncap.DON) error { + if config == nil { + p.lggr.Info("SetConfig called with nil config, using defaults") + config = &commoncap.RemoteTriggerConfig{} + } + config.ApplyDefaults() + if underlying == nil { + return errors.New("underlying trigger capability cannot be nil") + } + if capDonInfo.ID == 0 || len(capDonInfo.Members) == 0 { + return errors.New("empty capDonInfo provided") + } + if workflowDONs == nil { + workflowDONs = make(map[uint32]commoncap.DON) + } + + // Build the members cache + membersCache := make(map[uint32]map[rage.PeerID]bool) + for id, don := range workflowDONs { + cache := make(map[rage.PeerID]bool) + for _, member := range don.Members { + cache[member] = true + } + membersCache[id] = cache + } + + // always replace the whole dynamicPublisherConfig object to avoid inconsistent state + p.cfg.Store(&dynamicPublisherConfig{ + remoteConfig: config, + underlying: underlying, + capDonInfo: capDonInfo, + workflowDONs: workflowDONs, + membersCache: membersCache, + batchingEnabled: config.MaxBatchSize > 1 && config.BatchCollectionPeriod >= minAllowedBatchCollectionPeriod, + }) + + return nil +} + +func (p *triggerPublisher) initMetrics() error { + var err error + p.metrics.registerTriggerCounter, err = beholder.GetMeter().Int64Counter("platform_trigger_publisher_register_trigger_total") + if err != nil { + return fmt.Errorf("failed to register platform_trigger_publisher_register_trigger_total: %w", err) + } + p.metrics.unregisterTriggerCounter, err = beholder.GetMeter().Int64Counter("platform_trigger_publisher_unregister_trigger_total") + if err != nil { + return fmt.Errorf("failed to register platform_trigger_publisher_unregister_trigger_total: %w", err) + } + p.metrics.ackEventCounter, err = beholder.GetMeter().Int64Counter("platform_trigger_publisher_ack_event_total") + if err != nil { + return fmt.Errorf("failed to register platform_trigger_publisher_ack_event_total: %w", err) + } + durationBuckets := metric.WithExplicitBucketBoundaries(1, 50, 250, 1_000, 5_000, 30_000) + p.metrics.registerTriggerDurationMs, err = beholder.GetMeter().Int64Histogram("platform_trigger_publisher_register_trigger_duration_ms", durationBuckets) + if err != nil { + return fmt.Errorf("failed to register platform_trigger_publisher_register_trigger_duration_ms: %w", err) + } + p.metrics.unregisterTriggerDurationMs, err = beholder.GetMeter().Int64Histogram("platform_trigger_publisher_unregister_trigger_duration_ms", durationBuckets) + if err != nil { + return fmt.Errorf("failed to register platform_trigger_publisher_unregister_trigger_duration_ms: %w", err) + } + p.metrics.ackEventDurationMs, err = beholder.GetMeter().Int64Histogram("platform_trigger_publisher_ack_event_duration_ms", durationBuckets) + if err != nil { + return fmt.Errorf("failed to register platform_trigger_publisher_ack_event_duration_ms: %w", err) + } + return nil +} + +func (p *triggerPublisher) recordMethodDuration(ctx context.Context, hist metric.Int64Histogram, outcome string, d time.Duration) { + hist.Record(ctx, d.Milliseconds(), metric.WithAttributes( + attribute.String("capabilityID", p.capabilityID), + attribute.String("capMethodName", p.capMethodName), + attribute.String("outcome", outcome), + )) +} + +func (p *triggerPublisher) Start(ctx context.Context) error { + cfg := p.cfg.Load() + + // Validate that all required fields are set before starting + if cfg == nil { + return errors.New("config not set - call SetConfig() before Start()") + } + if cfg.remoteConfig == nil { + return errors.New("remoteConfig not set - call SetConfig() before Start()") + } + if cfg.underlying == nil { + return errors.New("underlying trigger capability not set - call SetConfig() before Start()") + } + if len(cfg.capDonInfo.Members) == 0 { + return errors.New("capability DON info not set - call SetConfig() before Start()") + } + if p.dispatcher == nil { + return errors.New("dispatcher set to nil, cannot start triggerPublisher") + } + if err := p.initMetrics(); err != nil { + return fmt.Errorf("failed to initialize metrics: %w", err) + } + + p.wg.Add(1) + go p.cacheCleanupLoop() + p.wg.Add(1) + go p.registrationCheckLoop() + p.wg.Add(1) + go p.batchingLoop() + + if err := p.ackExecutor.Start(ctx); err != nil { + return fmt.Errorf("failed to start ack executor: %w", err) + } + if err := p.registerExecutor.Start(ctx); err != nil { + return fmt.Errorf("failed to start register executor: %w", err) + } + + p.lggr.Info("TriggerPublisher started") + return nil +} + +// TODO: Reduce complexity -> https://smartcontract-it.atlassian.net/browse/PLEX-3258 +func (p *triggerPublisher) Receive(ctx context.Context, msg *types.MessageBody) { + cfg := p.cfg.Load() + if cfg == nil { + p.lggr.Errorw("received message but config is not set") + return + } + + sender, err := ToPeerID(msg.Sender) + if err != nil { + p.lggr.Errorw("failed to convert message sender to PeerID", "err", err) + return + } + + if msg.ErrorMsg != "" { + p.lggr.Errorw("received a message with error", + "method", SanitizeLogString(msg.Method), "sender", sender, "errorMsg", SanitizeLogString(msg.ErrorMsg)) + } + + switch msg.Method { + case types.MethodRegisterTrigger: + registerStart := time.Now() + registerOutcome := "invalid" + // Duration is recorded either by this defer (synchronous Receive exits) or by the + // register executor goroutine (success/error). recordRegisterDuration=false on + // successful schedule prevents double-counting. + // + // Interpreting outcomes: + // - success/error: quorum reached, RegisterTrigger ran (includes executor queue + // wait + underlying work such as DB). Use for end-to-end register latency. + // - anything else (not_ready, quorum_already_met, skipped_exists, schedule_failed, ...): + // returned without calling RegisterTrigger. These did early-exit as intended. + // High p99 here means Receive itself was slow—typically p.mu wait and/or + // time blocked acquiring an executor slot (schedule_failed), not DB writes. + recordRegisterDuration := true + defer func() { + if recordRegisterDuration { + p.recordMethodDuration(ctx, p.metrics.registerTriggerDurationMs, registerOutcome, time.Since(registerStart)) + } + }() + + req, unmarshalErr := pb.UnmarshalTriggerRegistrationRequest(msg.Payload) + if unmarshalErr != nil { + p.lggr.Errorw("failed to unmarshal trigger registration request", "err", unmarshalErr) + return + } + callerDon, ok := cfg.workflowDONs[msg.CallerDonId] + if !ok { + p.lggr.Errorw("received a message from unsupported workflow DON", "callerDonId", msg.CallerDonId) + return + } + if !cfg.membersCache[msg.CallerDonId][sender] { + p.lggr.Errorw("sender not a member of its workflow DON", "callerDonId", msg.CallerDonId, "sender", sender) + return + } + if err = validation.ValidateWorkflowOrExecutionID(req.Metadata.WorkflowID); err != nil { + p.lggr.Errorw("received trigger request with invalid workflow ID", "workflowId", SanitizeLogString(req.Metadata.WorkflowID), "err", err) + return + } + p.lggr.Debugw("received trigger registration", "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, "sender", sender) + key := registrationKey{msg.CallerDonId, req.Metadata.WorkflowID, req.TriggerID} + nowMs := time.Now().UnixMilli() + p.mu.Lock() + p.messageCache.Insert(key, sender, nowMs, msg.Payload) + if existing, exists := p.registrations[key]; exists { + p.mu.Unlock() + if existing.registrationErr != nil { + p.lggr.Debugw("skipping trigger registration; previous attempt failed with user error", + "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, "err", existing.registrationErr) + } else { + p.lggr.Debugw("trigger registration already exists", "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID) + } + return + } + // NOTE: require 2F+1 by default, introduce different strategies later (KS-76) + minRequired := uint32(2*callerDon.F + 1) + // once=true: only one register attempt per registration cycle. Whichever workflow + // node message satisfies quorum schedules RegisterTrigger; the following messages + // from other nodes do not (Ready returns false, WasReady returns true). If that attempt fails, registration + // must wait for the next cycle (e.g. re-quorum after messageCache.Delete on system + // error, or a fresh registration flow). + // + // This did not matter when registration was synchronous—the "already exists" check + // above returned early without causing unncessary RegisterTrigger calls. With async registration, + // that check may pass in time, so once=true prevents duplicate RegisterTrigger calls for the same trigger. + ready, payloads := p.messageCache.Ready(key, minRequired, nowMs-cfg.remoteConfig.RegistrationExpiry.Milliseconds(), true) + if !ready { + // Read cache state before unlocking; the register executor goroutine may + // mutate messageCache (Delete on system error) concurrently under p.mu. + peersReceived := len(p.messageCache.Peers(key)) + wasReady := p.messageCache.WasReady(key) + p.mu.Unlock() + if wasReady { + p.lggr.Debugw("quorum already met; ignoring duplicate registration message", + "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, + "peersReceived", peersReceived, "minRequired", minRequired) + } else { + p.lggr.Debugw("quorum not reached yet", + "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, + "peersReceived", peersReceived, "minRequired", minRequired) + } + + return + } + aggregated, aggregateErr := aggregation.AggregateModeRaw(payloads, uint32(callerDon.F+1)) + if aggregateErr != nil { + p.mu.Unlock() + p.lggr.Errorw("failed to aggregate trigger registrations", "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, "err", aggregateErr) + return + } + unmarshaled, unmarshalErr := pb.UnmarshalTriggerRegistrationRequest(aggregated) + if unmarshalErr != nil { + p.mu.Unlock() + p.lggr.Errorw("failed to unmarshal request", "err", unmarshalErr) + return + } + p.mu.Unlock() + + capAttrs := metric.WithAttributes( + attribute.String("capabilityID", p.capabilityID), + attribute.String("callerDonID", strconv.FormatUint(uint64(key.callerDonID), 10)), + ) + scheduleErr := p.registerExecutor.ExecuteTask(ctx, func(taskCtx context.Context) { + callbackCh, registerErr := cfg.underlying.RegisterTrigger(taskCtx, unmarshaled) + taskOutcome := "success" + if registerErr != nil { + taskOutcome = "error" + } + p.recordMethodDuration(taskCtx, p.metrics.registerTriggerDurationMs, taskOutcome, time.Since(registerStart)) + + p.mu.Lock() + defer p.mu.Unlock() + if registerErr == nil { + p.metrics.registerTriggerCounter.Add(taskCtx, 1, capAttrs, metric.WithAttributes(attribute.String("outcome", "success"))) + p.registrations[key] = &pubRegState{ + callback: callbackCh, + request: unmarshaled, + } + p.wg.Add(1) + go p.triggerEventLoop(callbackCh, key) + p.lggr.Debugw("updated trigger registration", "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID) + return + } + + p.metrics.registerTriggerCounter.Add(taskCtx, 1, capAttrs, metric.WithAttributes(attribute.String("outcome", "error"))) + var capErr caperrors.Error + if errors.As(registerErr, &capErr) && capErr.Origin() == caperrors.OriginUser { + p.registrations[key] = &pubRegState{registrationErr: registerErr} + p.lggr.Errorw("trigger registration failed with user error; will not retry", + "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, "err", registerErr) + return + } + // System errors should retry. Ready(..., once=true) sets wasReady on quorum and never + // resets it; without Delete, later messages keep getting not_ready even though we + // never registered. Delete clears wasReady so a fresh quorum can schedule again. + p.lggr.Errorw("trigger registration failed with system error; will retry", + "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, "err", registerErr) + p.messageCache.Delete(key) + }) + if scheduleErr != nil { + p.lggr.Errorw("failed to schedule RegisterTrigger task", + "workflowId", req.Metadata.WorkflowID, "triggerID", req.TriggerID, "err", scheduleErr) + return + } + // Hand off to executor; disable defer recording—goroutine records success/error. + recordRegisterDuration = false + case types.MethodUnregisterTrigger: + unregisterStart := time.Now() + unregisterOutcome := "invalid" + defer func() { + p.recordMethodDuration(ctx, p.metrics.unregisterTriggerDurationMs, unregisterOutcome, time.Since(unregisterStart)) + }() + + meta := msg.GetTriggerEventMetadata() + if meta == nil { + p.lggr.Errorw("received unregister with nil metadata", "sender", sender) + return + } + if len(meta.WorkflowIds) != 1 || len(meta.TriggerIds) != 1 { + p.lggr.Errorw("received unregister with unexpected metadata sizes", + "sender", sender, "workflowIdsLen", len(meta.WorkflowIds), "triggerIdsLen", len(meta.TriggerIds)) + return + } + callerDon, ok := cfg.workflowDONs[msg.CallerDonId] + if !ok { + p.lggr.Errorw("received unregister from unsupported workflow DON", "callerDonId", msg.CallerDonId) + return + } + if !cfg.membersCache[msg.CallerDonId][sender] { + p.lggr.Errorw("unregister sender not a member of its workflow DON", "callerDonId", msg.CallerDonId, "sender", sender) + return + } + + key := registrationKey{ + callerDonID: msg.CallerDonId, + workflowID: meta.WorkflowIds[0], + triggerID: meta.TriggerIds[0], + } + + p.mu.Lock() + reg, exists := p.registrations[key] + if !exists { + p.mu.Unlock() + return + } + nowMs := time.Now().UnixMilli() + minRequired := uint32(2*callerDon.F + 1) + p.unregisterCache.Insert(key, sender, nowMs, nil) + ready, _ := p.unregisterCache.Ready(key, minRequired, nowMs-cfg.remoteConfig.RegistrationExpiry.Milliseconds(), true) + if !ready { + peersReceived := len(p.unregisterCache.Peers(key)) + if p.unregisterCache.WasReady(key) { + p.mu.Unlock() + p.lggr.Debugw("unregister quorum already met; ignoring duplicate message", + "workflowID", key.workflowID, "triggerID", key.triggerID, "sender", sender, + "peersReceived", peersReceived, "minRequired", minRequired) + } else { + p.mu.Unlock() + p.lggr.Debugw("unregister quorum not reached yet", + "workflowID", key.workflowID, "triggerID", key.triggerID, "sender", sender, + "peersReceived", peersReceived, "minRequired", minRequired) + } + return + } + p.lggr.Debugw("unregister trigger", "workflowID", key.workflowID, "triggerID", key.triggerID, + "callerDonId", msg.CallerDonId, "minRequired", minRequired, "sender", sender) + delete(p.registrations, key) + p.unregisterCache.Delete(key) + p.messageCache.Delete(key) + p.mu.Unlock() + + ctx, cancel := p.stopCh.NewCtx() + err = p.cfg.Load().underlying.UnregisterTrigger(ctx, reg.request) + if err != nil { + unregisterOutcome = "error" + p.lggr.Errorw("failed to unregister trigger on underlying", "workflowID", key.workflowID, "triggerID", key.triggerID, "err", err) + } else { + unregisterOutcome = "success" + } + cancel() + p.lggr.Infow("unregistered trigger", "workflowID", key.workflowID, "triggerID", key.triggerID) + case types.MethodTriggerEvent: + p.lggr.Errorw("trigger request failed with error", + "method", SanitizeLogString(msg.Method), "sender", sender, "errorMsg", SanitizeLogString(msg.ErrorMsg)) + case types.MethodTriggerEventAck: + ackHandlerStart := time.Now() + ackOutcome := "invalid" + // Same two-path duration model as register: defer for synchronous Receive exits, + // executor goroutine for success/error after quorum. See register case comment. + recordAckDuration := true + defer func() { + if recordAckDuration { + p.recordMethodDuration(ctx, p.metrics.ackEventDurationMs, ackOutcome, time.Since(ackHandlerStart)) + } + }() + + triggerMetadata := msg.GetTriggerEventMetadata() + if triggerMetadata == nil { + p.lggr.Errorw("received empty trigger event ack metadata", "sender", sender) + return + } + triggerEventID := triggerMetadata.TriggerEventId + p.lggr.Debugw("received trigger event ACK", "sender", sender, "trigger event ID", triggerEventID) + + p.mu.Lock() + callerDon, ok := cfg.workflowDONs[msg.CallerDonId] + if !ok { + p.mu.Unlock() + p.lggr.Errorw("received a message from unsupported workflow DON", "callerDonId", msg.CallerDonId) + return + } + if !cfg.membersCache[msg.CallerDonId][sender] { + p.mu.Unlock() + p.lggr.Errorw("sender not a member of its workflow DON", "callerDonId", msg.CallerDonId, "sender", sender) + return + } + if len(triggerMetadata.TriggerIds) != 1 { + p.mu.Unlock() + p.lggr.Errorw("did not receive single triggerID in ACK request", "callerDonId", msg.CallerDonId, "sender", sender, "triggerIDs", triggerMetadata.TriggerIds) + return + } + triggerID := triggerMetadata.TriggerIds[0] + + key := ackKey{msg.CallerDonId, triggerEventID, triggerID} + nowMs := time.Now().UnixMilli() + p.ackCache.Insert(key, sender, nowMs, msg.Payload) + minRequired := uint32(2*callerDon.F + 1) + // false is set to to return ready even after quorum + // to add redundancy in case AckEvent fails below. + ready, _ := p.ackCache.Ready(key, minRequired, 0, false) + ackCount := len(p.ackCache.Peers(key)) + if !ready { + p.mu.Unlock() + p.lggr.Debugw("ACK quorum not reached yet", + "triggerEventId", triggerEventID, + "triggerID", triggerID, + "acksReceived", ackCount, + "minRequired", minRequired) + return + } + + p.mu.Unlock() + + callerDonID := msg.CallerDonId + ackAttrs := metric.WithAttributes( + attribute.String("capabilityID", p.capabilityID), + attribute.String("callerDonID", strconv.FormatUint(uint64(callerDonID), 10)), + ) + + p.lggr.Infow("ACK quorum reached, forwarding to underlying trigger", + "triggerEventId", triggerEventID, + "triggerID", triggerID, + "minRequired", minRequired) + + scheduleErr := p.ackExecutor.ExecuteTask(ctx, func(taskCtx context.Context) { + p.lggr.Infow("executing AckEvent task", + "triggerEventId", triggerEventID, + "triggerID", triggerID, + "callerDonId", callerDonID, + "minRequired", minRequired) + ackErr := cfg.underlying.AckEvent(taskCtx, triggerID, triggerEventID, p.capMethodName) + taskOutcome := "success" + if ackErr != nil { + taskOutcome = "error" + } + p.recordMethodDuration(taskCtx, p.metrics.ackEventDurationMs, taskOutcome, time.Since(ackHandlerStart)) + if ackErr != nil { + p.metrics.ackEventCounter.Add(taskCtx, 1, ackAttrs, metric.WithAttributes(attribute.String("outcome", "error"))) + p.lggr.Errorw("failed to AckEvent on underlying trigger capability", + "eventID", triggerEventID, "capabilityID", p.capabilityID, "err", ackErr) + return + } + p.lggr.Infow("AckEvent completed successfully", + "triggerEventId", triggerEventID, + "triggerID", triggerID, + "callerDonId", callerDonID, + "minRequired", minRequired) + p.metrics.ackEventCounter.Add(taskCtx, 1, ackAttrs, metric.WithAttributes(attribute.String("outcome", "success"))) + }) + if scheduleErr != nil { + p.lggr.Errorw("failed to schedule AckEvent task", + "triggerEventId", triggerEventID, "triggerID", triggerID, "err", scheduleErr) + return + } + recordAckDuration = false + default: + p.lggr.Errorw("received message with unknown method", + "method", SanitizeLogString(msg.Method), "sender", sender) + } +} + +func (p *triggerPublisher) cacheCleanupLoop() { + defer p.wg.Done() + + // Get initial config for ticker setup + firstCfg := p.cfg.Load() + if firstCfg == nil { + p.lggr.Errorw("cacheCleanupLoop started but config not set") + return + } + cleanupInterval := firstCfg.remoteConfig.MessageExpiry + ticker := time.NewTicker(cleanupInterval) + defer ticker.Stop() + for { + select { + case <-p.stopCh: + return + case <-ticker.C: + cfg := p.cfg.Load() + if cfg.remoteConfig.MessageExpiry != cleanupInterval { + cleanupInterval = cfg.remoteConfig.MessageExpiry + ticker.Reset(cleanupInterval) + } + now := time.Now().UnixMilli() + + // Registrations are no longer expired by timeout. Subscribers that + // no longer want a registration respond to MethodTriggerRegistrationCheck + // with MethodUnregisterTrigger, which is handled in Receive. + p.mu.Lock() + deleted := p.ackCache.DeleteOlderThan(now - cfg.remoteConfig.MessageExpiry.Milliseconds()) + p.mu.Unlock() + + if deleted > 0 { + p.lggr.Debugw("cleaned expired AckCache entries", "deleted", deleted) + } + } + } +} + +func (p *triggerPublisher) registrationCheckLoop() { + defer p.wg.Done() + firstCfg := p.cfg.Load() + if firstCfg == nil { + p.lggr.Errorw("registrationCheckLoop started but config not set") + return + } + checkInterval := firstCfg.remoteConfig.RegistrationRefresh + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + for { + select { + case <-p.stopCh: + return + case <-ticker.C: + cfg := p.cfg.Load() + if cfg.remoteConfig.RegistrationRefresh != checkInterval { + checkInterval = cfg.remoteConfig.RegistrationRefresh + ticker.Reset(checkInterval) + } + p.sendRegistrationChecks() + } + } +} + +func (p *triggerPublisher) sendRegistrationChecks() { + cfg := p.cfg.Load() + if cfg == nil { + return + } + + now := time.Now().UnixMilli() + + p.mu.Lock() + deletedUnreg := p.unregisterCache.DeleteOlderThan(now - cfg.remoteConfig.RegistrationExpiry.Milliseconds()) + p.mu.Unlock() + + if deletedUnreg > 0 { + p.lggr.Debugw("cleaned expired unregister cache entries", "deleted", deletedUnreg) + } + + p.mu.RLock() + totalRegistrations := len(p.registrations) + grouped := make(map[uint32][]registrationKey, len(cfg.workflowDONs)) + for key := range p.registrations { + grouped[key.callerDonID] = append(grouped[key.callerDonID], key) + } + p.mu.RUnlock() + + p.lggr.Infow("sendRegistrationChecks: tick", + "totalRegistrations", totalRegistrations, + "nWorkflowDONs", len(grouped)) + + for callerDonID, keys := range grouped { + if len(keys) == 0 { + continue + } + + chunkSize := int(cfg.remoteConfig.MaxBatchSize) + if chunkSize < 1 { + chunkSize = commoncap.DefaultBatchSize + } + + don, ok := cfg.workflowDONs[callerDonID] + if !ok { + continue + } + + for offset := 0; offset < len(keys); offset += chunkSize { + end := min(offset+chunkSize, len(keys)) + chunk := keys[offset:end] + workflowIDs := make([]string, len(chunk)) + triggerIDs := make([]string, len(chunk)) + for i, key := range chunk { + workflowIDs[i] = key.workflowID + triggerIDs[i] = key.triggerID + } + + msg := &types.MessageBody{ + CapabilityId: p.capabilityID, + CapabilityDonId: cfg.capDonInfo.ID, + CallerDonId: callerDonID, + Method: types.MethodTriggerRegistrationCheck, + CapabilityMethod: p.capMethodName, + Metadata: &types.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &types.TriggerEventMetadata{ + WorkflowIds: workflowIDs, + TriggerIds: triggerIDs, + }, + }, + } + + for _, peerID := range don.Members { + err := p.dispatcher.Send(peerID, msg) + if err != nil { + p.lggr.Errorw("failed to send message", "donId", cfg.capDonInfo.ID, "peerId", peerID, "err", err) + } + } + p.lggr.Debugw("sent batched registration check", + "callerDonId", callerDonID, + "chunkOffset", offset, + "chunkLen", len(chunk), + "totalRegistrations", len(keys), + "nPeers", len(don.Members)) + } + } +} + +func (p *triggerPublisher) triggerEventLoop(callbackCh <-chan commoncap.TriggerResponse, key registrationKey) { + defer p.wg.Done() + for { + select { + case <-p.stopCh: + return + case response, ok := <-callbackCh: + if !ok { + p.lggr.Infow("triggerEventLoop channel closed", "workflowId", key.workflowID, "triggerID", key.triggerID) + return + } + + triggerEvent := response.Event + p.lggr.Debugw("received trigger event", "workflowId", key.workflowID, "triggerID", key.triggerID, "triggerEventID", triggerEvent.ID) + marshaledResponse, err := pb.MarshalTriggerResponse(response) + if err != nil { + p.lggr.Debugw("can't marshal trigger event", "err", err) + break + } + + cfg := p.cfg.Load() + if cfg.batchingEnabled { + p.enqueueForBatching(marshaledResponse, key, triggerEvent.ID) + } else { + // a single-element "batch" + p.sendBatch(&batchedResponse{ + rawResponse: marshaledResponse, + callerDonID: key.callerDonID, + triggerEventID: triggerEvent.ID, + workflowIDs: []string{key.workflowID}, + triggerIDs: []string{key.triggerID}, + }) + } + } + } +} + +func (p *triggerPublisher) enqueueForBatching(rawResponse []byte, key registrationKey, triggerEventID string) { + // put in batching queue, group by hash(callerDonId, triggerEventID, response) + combined := make([]byte, 0, 4+len(triggerEventID)+len(rawResponse)) + combined = binary.LittleEndian.AppendUint32(combined, key.callerDonID) + combined = append(combined, triggerEventID...) + combined = append(combined, rawResponse...) + sha := sha256.Sum256(combined) + p.bqMu.Lock() + elem, exists := p.batchingQueue[sha] + if !exists { + elem = &batchedResponse{ + rawResponse: rawResponse, + callerDonID: key.callerDonID, + triggerEventID: triggerEventID, + workflowIDs: []string{key.workflowID}, + triggerIDs: []string{key.triggerID}, + } + p.batchingQueue[sha] = elem + } else { + elem.workflowIDs = append(elem.workflowIDs, key.workflowID) + elem.triggerIDs = append(elem.triggerIDs, key.triggerID) + } + p.bqMu.Unlock() +} + +func (p *triggerPublisher) sendBatch(resp *batchedResponse) { + cfg := p.cfg.Load() + if cfg == nil { + p.lggr.Errorw("config not set during sendBatch") + return + } + + for len(resp.workflowIDs) > 0 { + workflowBatch := resp.workflowIDs + triggerBatch := resp.triggerIDs + if cfg.batchingEnabled && int64(len(workflowBatch)) > int64(cfg.remoteConfig.MaxBatchSize) { + workflowBatch = workflowBatch[:cfg.remoteConfig.MaxBatchSize] + triggerBatch = triggerBatch[:cfg.remoteConfig.MaxBatchSize] + resp.workflowIDs = resp.workflowIDs[cfg.remoteConfig.MaxBatchSize:] + resp.triggerIDs = resp.triggerIDs[cfg.remoteConfig.MaxBatchSize:] + } else { + resp.workflowIDs = nil + resp.triggerIDs = nil + } + + peersSent := 0 + for _, peerID := range cfg.workflowDONs[resp.callerDonID].Members { + peersSent++ + + p.lggr.Debugw("sending trigger event to peer", + "peerID", peerID, + "callerDonID", resp.callerDonID, + "triggerEventID", resp.triggerEventID, + "workflowIDs", workflowBatch, + "triggerIDs", triggerBatch, + ) + + msg := &types.MessageBody{ + CapabilityId: p.capabilityID, + CapabilityDonId: cfg.capDonInfo.ID, + CallerDonId: resp.callerDonID, + Method: types.MethodTriggerEvent, + Payload: resp.rawResponse, + CapabilityMethod: p.capMethodName, + Metadata: &types.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &types.TriggerEventMetadata{ + WorkflowIds: workflowBatch, + TriggerIds: triggerBatch, + TriggerEventId: resp.triggerEventID, + }, + }, + } + + err := p.dispatcher.Send(peerID, msg) + if err != nil { + p.lggr.Errorw("failed to send trigger event", "peerID", peerID, "err", err) + } + } + p.lggr.Infow("sendBatch: event dispatched", + "triggerEventID", resp.triggerEventID, + "callerDonID", resp.callerDonID, + "batchSize", len(triggerBatch), + "peersSent", peersSent) + } +} + +func (p *triggerPublisher) batchingLoop() { + defer p.wg.Done() + + // Get initial config for ticker setup + firstCfg := p.cfg.Load() + if firstCfg == nil { + p.lggr.Errorw("batchingLoop started but config not set") + return + } + interval := firstCfg.remoteConfig.BatchCollectionPeriod + ticker := time.NewTicker(interval) + + defer ticker.Stop() + for { + select { + case <-p.stopCh: + return + case <-ticker.C: + cfg := p.cfg.Load() + // Update cleanup interval if config has changed + if cfg.remoteConfig.MessageExpiry != interval { + interval = cfg.remoteConfig.BatchCollectionPeriod + ticker.Reset(interval) + } + + p.bqMu.Lock() + queue := p.batchingQueue + p.batchingQueue = make(map[[32]byte]*batchedResponse) + p.bqMu.Unlock() + + for _, elem := range queue { + p.sendBatch(elem) + } + } + } +} + +func (p *triggerPublisher) Close() error { + close(p.stopCh) + + if p.ackExecutor != nil { + if err := p.ackExecutor.Close(); err != nil { + return fmt.Errorf("failed to close ack executor: %w", err) + } + } + if p.registerExecutor != nil { + if err := p.registerExecutor.Close(); err != nil { + return fmt.Errorf("failed to close register executor: %w", err) + } + } + + p.wg.Wait() + p.lggr.Info("TriggerPublisher closed") + return nil +} + +func (p *triggerPublisher) Ready() error { + return nil +} + +func (p *triggerPublisher) HealthReport() map[string]error { + return nil +} + +func (p *triggerPublisher) Name() string { + return p.lggr.Name() +} diff --git a/libs/x/don2don/trigger_publisher_test.go b/libs/x/don2don/trigger_publisher_test.go new file mode 100644 index 000000000..da8fda85f --- /dev/null +++ b/libs/x/don2don/trigger_publisher_test.go @@ -0,0 +1,1376 @@ +package don2don_test + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/don2don/types/mocks" + "github.com/smartcontractkit/capabilities/libs/x/internal/synctest" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +const capID = "cap_id@1" + +func TestTriggerPublisher_Register(t *testing.T) { + t.Parallel() + + ctx := t.Context() + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + underlyingTriggerCap, publisher, _, peers := newServices(t, capabilityDONID, workflowDONID, 1, time.Second) + + // invalid sender case - node 0 is not a member of the workflow DON, registration shoudn't happen + regEvent := newRegisterTriggerMessage(t, workflowDONID, peers[0]) + publisher.Receive(ctx, regEvent) + require.Empty(t, underlyingTriggerCap.registrationsCh) + + // valid registration + regEvent = newRegisterTriggerMessage(t, workflowDONID, peers[1]) + publisher.Receive(ctx, regEvent) + require.Eventually(t, func() bool { return len(underlyingTriggerCap.registrationsCh) > 0 }, 2*time.Second, 10*time.Millisecond) + forwarded := <-underlyingTriggerCap.registrationsCh + require.Equal(t, workflowID1, forwarded.Metadata.WorkflowID) + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_ReceiveTriggerEvents_NoBatching(t *testing.T) { + t.Parallel() + + ctx := t.Context() + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + underlyingTriggerCap, publisher, dispatcher, peers := newServices(t, capabilityDONID, workflowDONID, 1, time.Second) + regEvent := newRegisterTriggerMessage(t, workflowDONID, peers[1]) + publisher.Receive(ctx, regEvent) + require.Eventually(t, func() bool { return len(underlyingTriggerCap.registrationsCh) > 0 }, 2*time.Second, 10*time.Millisecond) + + // send a trigger event and expect that it gets delivered right away + awaitOutgoingMessageCh := make(chan struct{}) + dispatcher.On("Send", peers[1], mock.Anything).Run(func(args mock.Arguments) { + awaitOutgoingMessageCh <- struct{}{} + }).Return(nil) + underlyingTriggerCap.eventCh <- commoncap.TriggerResponse{} + <-awaitOutgoingMessageCh + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_ReceiveTriggerEvents_BatchingEnabled(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + const batchPeriod = 50 * time.Millisecond + ctx := t.Context() + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + underlyingTriggerCap, publisher, dispatcher, peers := newServices(t, capabilityDONID, workflowDONID, 2, batchPeriod) + regEvent := newRegisterTriggerMessage(t, workflowDONID, peers[1]) + publisher.Receive(ctx, regEvent) + require.Eventually(t, func() bool { return len(underlyingTriggerCap.registrationsCh) > 0 }, 2*time.Second, 10*time.Millisecond) + + awaitOutgoingMessageCh := make(chan struct{}, 1) + + // send two trigger events and expect them to be delivered in a batch + dispatcher.On("Send", peers[1], mock.Anything).Run(func(args mock.Arguments) { + msg := args.Get(1).(*don2dontypes.MessageBody) + require.Equal(t, capID, msg.CapabilityId) + require.Equal(t, don2dontypes.MethodTriggerEvent, msg.Method) + require.NotEmpty(t, msg.Payload) + metadata := msg.Metadata.(*don2dontypes.MessageBody_TriggerEventMetadata) + require.Len(t, metadata.TriggerEventMetadata.WorkflowIds, 2) + awaitOutgoingMessageCh <- struct{}{} + }).Return(nil).Once() + underlyingTriggerCap.eventCh <- commoncap.TriggerResponse{} + underlyingTriggerCap.eventCh <- commoncap.TriggerResponse{} + time.Sleep(batchPeriod) + <-awaitOutgoingMessageCh + + // if there are fewer pending event than the batch size, + // the events should still be sent after the batch collection period + dispatcher.On("Send", peers[1], mock.Anything).Run(func(args mock.Arguments) { + msg := args.Get(1).(*don2dontypes.MessageBody) + metadata := msg.Metadata.(*don2dontypes.MessageBody_TriggerEventMetadata) + require.Len(t, metadata.TriggerEventMetadata.WorkflowIds, 1) + awaitOutgoingMessageCh <- struct{}{} + }).Return(nil).Once() + underlyingTriggerCap.eventCh <- commoncap.TriggerResponse{} + time.Sleep(batchPeriod) + <-awaitOutgoingMessageCh + + require.NoError(t, publisher.Close()) + }) +} + +func TestTriggerPublisher_ReceiveTriggerEventAcks(t *testing.T) { + t.Parallel() + + ctx := t.Context() + capabilityDONID, workflowDONID := uint32(1), uint32(2) + underlyingTriggerCap, publisher, _, peers := newServices(t, capabilityDONID, workflowDONID, 2, time.Second) + eventID := "123" + triggerID := "trigA" + regEvent := newAckEventMessage(t, eventID, triggerID, workflowDONID, peers[1]) + publisher.Receive(ctx, regEvent) + + require.Eventually(t, func() bool { return underlyingTriggerCap.ackCount.Load() == 1 }, 2*time.Second, 10*time.Millisecond) + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_SetConfig_Basic(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + capDonInfo := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: 2, + Members: []rage.PeerID{peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + underlying := &testTrigger{ + info: capInfo, + registrationsCh: make(chan commoncap.TriggerRegistrationRequest, 2), + eventCh: make(chan commoncap.TriggerResponse, 2), + } + + t.Run("returns error when underlying trigger capability is nil", func(t *testing.T) { + t.Parallel() + + dispatcher := mocks.NewDispatcher(t) + publisher := don2don.NewTriggerPublisher(capInfo.ID, "method", dispatcher, lggr) + config := &commoncap.RemoteTriggerConfig{} + err := publisher.SetConfig(config, nil, capDonInfo, workflowDONs) + require.Error(t, err) + require.Contains(t, err.Error(), "underlying trigger capability cannot be nil") + }) + + t.Run("handles nil config", func(t *testing.T) { + t.Parallel() + + dispatcher := mocks.NewDispatcher(t) + publisher := don2don.NewTriggerPublisher(capInfo.ID, "method", dispatcher, lggr) + // Set config as nil - should use defaults + err := publisher.SetConfig(nil, underlying, capDonInfo, workflowDONs) + require.NoError(t, err) + + // Verify config works + ctx := t.Context() + require.NoError(t, publisher.Start(ctx)) + require.NoError(t, publisher.Close()) + }) + + t.Run("handles nil workflowDONs", func(t *testing.T) { + t.Parallel() + + dispatcher := mocks.NewDispatcher(t) + publisher := don2don.NewTriggerPublisher(capInfo.ID, "method", dispatcher, lggr) + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + } + // Set workflowDONs as nil - should create empty map + err := publisher.SetConfig(config, underlying, capDonInfo, nil) + require.NoError(t, err) + + // Verify config works + ctx := t.Context() + require.NoError(t, publisher.Start(ctx)) + require.NoError(t, publisher.Close()) + }) + + t.Run("updates existing config", func(t *testing.T) { + t.Parallel() + + dispatcher := mocks.NewDispatcher(t) + publisher := don2don.NewTriggerPublisher(capInfo.ID, "method", dispatcher, lggr) + // Set initial config + initialConfig := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: 100 * time.Millisecond, + } + err := publisher.SetConfig(initialConfig, underlying, capDonInfo, workflowDONs) + require.NoError(t, err) + + // Update with new config + newConfig := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 500 * time.Millisecond, + RegistrationExpiry: 500 * time.Second, + MinResponsesToAggregate: 3, + MessageExpiry: 500 * time.Second, + MaxBatchSize: 5, + BatchCollectionPeriod: 500 * time.Millisecond, + } + err = publisher.SetConfig(newConfig, underlying, capDonInfo, workflowDONs) + require.NoError(t, err) + + // Verify updated config works + ctx := t.Context() + require.NoError(t, publisher.Start(ctx)) + require.NoError(t, publisher.Close()) + }) +} + +func newServices(t *testing.T, capabilityDONID uint32, workflowDONID uint32, maxBatchSize uint32, batchCollectionPeriod time.Duration) (*testTrigger, don2dontypes.ReceiverService, *mocks.Dispatcher, []rage.PeerID) { + t.Helper() + lggr := logger.Test(t) + ctx := t.Context() + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, // peer 0 is in the capability DON + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1]}, // peer 1 is in the workflow DON + F: 0, + } + + dispatcher := mocks.NewDispatcher(t) + allowRegistrationChecks(dispatcher) + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: maxBatchSize, + BatchCollectionPeriod: batchCollectionPeriod, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + underlying := &testTrigger{ + info: capInfo, + registrationsCh: make(chan commoncap.TriggerRegistrationRequest, 2), + eventCh: make(chan commoncap.TriggerResponse, 2), + } + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + return underlying, publisher, dispatcher, peers +} + +func allowRegistrationChecks(dispatcher *mocks.Dispatcher) { + dispatcher.On("Send", mock.Anything, mock.MatchedBy(func(m *don2dontypes.MessageBody) bool { + return m.Method == don2dontypes.MethodTriggerRegistrationCheck + })).Return(nil).Maybe() +} + +func newRegisterTriggerMessage(t *testing.T, callerDonID uint32, sender rage.PeerID) *don2dontypes.MessageBody { + // trigger registration event + triggerRequest := commoncap.TriggerRegistrationRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + marshaled, err := pb.MarshalTriggerRegistrationRequest(triggerRequest) + require.NoError(t, err) + return &don2dontypes.MessageBody{ + Sender: sender[:], + Method: don2dontypes.MethodRegisterTrigger, + CallerDonId: callerDonID, + Payload: marshaled, + } +} + +func newAckEventMessage(t *testing.T, eventID string, triggerID string, callerDonID uint32, sender rage.PeerID) *don2dontypes.MessageBody { + return &don2dontypes.MessageBody{ + Sender: sender[:], + Method: don2dontypes.MethodTriggerEventAck, + CallerDonId: callerDonID, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + TriggerEventId: eventID, + TriggerIds: []string{triggerID}, + }, + }, + } +} + +type testTrigger struct { + info commoncap.CapabilityInfo + registrationsCh chan commoncap.TriggerRegistrationRequest + eventCh chan commoncap.TriggerResponse + eventAckd bool + ackCount atomic.Int32 + ackBlock chan struct{} +} + +func (tr *testTrigger) Info(_ context.Context) (commoncap.CapabilityInfo, error) { + return tr.info, nil +} + +// waitForRegistration blocks until async RegisterTrigger delivers to the underlying mock. +func waitForRegistration(t *testing.T, ch <-chan commoncap.TriggerRegistrationRequest) commoncap.TriggerRegistrationRequest { + t.Helper() + require.Eventually(t, func() bool { return len(ch) > 0 }, 2*time.Second, 10*time.Millisecond) + return <-ch +} + +// waitForUnregister blocks until UnregisterTrigger runs on the underlying mock. +func waitForUnregister(t *testing.T, ch <-chan string) string { + t.Helper() + require.Eventually(t, func() bool { return len(ch) > 0 }, 2*time.Second, 10*time.Millisecond) + return <-ch +} + +func (tr *testTrigger) RegisterTrigger(_ context.Context, request commoncap.TriggerRegistrationRequest) (<-chan commoncap.TriggerResponse, error) { + tr.registrationsCh <- request + return tr.eventCh, nil +} + +func (tr *testTrigger) UnregisterTrigger(_ context.Context, request commoncap.TriggerRegistrationRequest) error { + return nil +} + +func (tr *testTrigger) AckEvent(_ context.Context, triggerID string, eventID string, method string) error { + tr.eventAckd = true + tr.ackCount.Add(1) + if tr.ackBlock != nil { + <-tr.ackBlock + } + return nil +} + +func TestTriggerPublisher_AckEvent_DoesNotBlockReceive(t *testing.T) { + t.Parallel() + + ctx := t.Context() + capabilityDONID, workflowDONID := uint32(1), uint32(2) + underlying, publisher, _, peers := newServices(t, capabilityDONID, workflowDONID, 1, time.Second) + underlying.ackBlock = make(chan struct{}) + + eventID := "evt-block-test" + triggerID := "trigA" + publisher.Receive(ctx, newAckEventMessage(t, eventID, triggerID, workflowDONID, peers[1])) + + regMsg := newRegisterTriggerMessage(t, workflowDONID, peers[1]) + regDone := make(chan struct{}) + go func() { + publisher.Receive(ctx, regMsg) + close(regDone) + }() + + select { + case <-regDone: + case <-time.After(500 * time.Millisecond): + t.Fatal("RegisterTrigger Receive blocked while AckEvent in flight") + } + + close(underlying.ackBlock) + require.Eventually(t, func() bool { return underlying.ackCount.Load() == 1 }, 2*time.Second, 10*time.Millisecond) + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_MultipleTriggersSameWorkflow(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr := logger.Test(t) + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + // Create a trigger that tracks registrations by triggerID + underlying := newMultiTrigger(capInfo) + + dispatcher := mocks.NewDispatcher(t) + allowRegistrationChecks(dispatcher) + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, // no batching + BatchCollectionPeriod: time.Second, + } + + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + // Register trigger1 + regEvent1 := newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[1], "trigger1") + publisher.Receive(ctx, regEvent1) + reg1 := waitForRegistration(t, underlying.registrationsCh) + require.Equal(t, "trigger1", reg1.TriggerID) + require.Equal(t, workflowID1, reg1.Metadata.WorkflowID) + + // Register trigger2 for the same workflow + regEvent2 := newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[1], "trigger2") + publisher.Receive(ctx, regEvent2) + reg2 := waitForRegistration(t, underlying.registrationsCh) + require.Equal(t, "trigger2", reg2.TriggerID) + require.Equal(t, workflowID1, reg2.Metadata.WorkflowID) // same workflowID + + trigger1EventReceived := make(chan struct{}) + trigger2EventReceived := make(chan struct{}) + + dispatcher.On("Send", peers[1], mock.Anything).Run(func(args mock.Arguments) { + msg := args.Get(1).(*don2dontypes.MessageBody) + require.Equal(t, capID, msg.CapabilityId) + require.Equal(t, don2dontypes.MethodTriggerEvent, msg.Method) + metadata := msg.Metadata.(*don2dontypes.MessageBody_TriggerEventMetadata) + require.Len(t, metadata.TriggerEventMetadata.WorkflowIds, 1) + require.Len(t, metadata.TriggerEventMetadata.TriggerIds, 1) + triggerID := metadata.TriggerEventMetadata.TriggerIds[0] + eventID := metadata.TriggerEventMetadata.TriggerEventId + if triggerID == "trigger1" && eventID == "event1" { + close(trigger1EventReceived) + } else if triggerID == "trigger2" && eventID == "event2" { + close(trigger2EventReceived) + } + }).Return(nil) + + // Send both events and expect them to be delivered separately + underlying.SendEvent("trigger1", commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ID: "event1"}, + }) + underlying.SendEvent("trigger2", commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ID: "event2"}, + }) + + <-trigger1EventReceived + <-trigger2EventReceived + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_ExplicitUnregister(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr := logger.Test(t) + + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1]}, + F: 0, + } + + dispatcher := mocks.NewDispatcher(t) + allowRegistrationChecks(dispatcher) + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + underlying := newMultiTrigger(capInfo) + + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + // Register trigger + regEvent := newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[1], "triggerA") + publisher.Receive(ctx, regEvent) + + waitForRegistration(t, underlying.registrationsCh) + + // Send unregister + unregMsg := &don2dontypes.MessageBody{ + Sender: peers[1][:], + Method: don2dontypes.MethodUnregisterTrigger, + CallerDonId: workflowDONID, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID1}, + TriggerIds: []string{"triggerA"}, + }, + }, + } + publisher.Receive(ctx, unregMsg) + require.Equal(t, "triggerA", waitForUnregister(t, underlying.unregisterCalled)) + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_SendsRegistrationChecks(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + underlying := newMultiTrigger(capInfo) + dispatcher := mocks.NewDispatcher(t) + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + + checkReceived := make(chan *don2dontypes.MessageBody, 10) + dispatcher.On("Send", mock.Anything, mock.MatchedBy(func(m *don2dontypes.MessageBody) bool { + return m.Method == don2dontypes.MethodTriggerRegistrationCheck + })).Run(func(args mock.Arguments) { + msg := args.Get(1).(*don2dontypes.MessageBody) + checkReceived <- msg + }).Return(nil).Maybe() + + // Start before Receive so initMetrics() has run (RegisterTrigger success path records metrics). + require.NoError(t, publisher.Start(ctx)) + + regEvent := newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[1], "triggerA") + publisher.Receive(ctx, regEvent) + waitForRegistration(t, underlying.registrationsCh) + + select { + case msg := <-checkReceived: + meta := msg.GetTriggerEventMetadata() + require.NotNil(t, meta) + require.Equal(t, []string{workflowID1}, meta.WorkflowIds) + require.Equal(t, []string{"triggerA"}, meta.TriggerIds) + require.Equal(t, capabilityDONID, msg.CapabilityDonId) + require.Equal(t, workflowDONID, msg.CallerDonId) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for registration check message") + } + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_RegistrationChecksChunkByMaxBatchSize(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + capDonInfo := commoncap.DON{ID: capabilityDONID, Members: []rage.PeerID{peers[0]}, F: 0} + workflowDonInfo := commoncap.DON{ID: workflowDONID, Members: []rage.PeerID{peers[1]}, F: 0} + workflowDONs := map[uint32]commoncap.DON{workflowDonInfo.ID: workflowDonInfo} + + const maxBatchSize uint32 = 100 + const nRegs = 250 + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: maxBatchSize, + BatchCollectionPeriod: time.Second, + } + + underlying := newMultiTrigger(capInfo) + dispatcher := mocks.NewDispatcher(t) + + var mu sync.Mutex + var chunkLens []int + dispatcher.On("Send", peers[1], mock.MatchedBy(func(m *don2dontypes.MessageBody) bool { + return m.Method == don2dontypes.MethodTriggerRegistrationCheck + })).Run(func(args mock.Arguments) { + msg := args.Get(1).(*don2dontypes.MessageBody) + meta := msg.GetTriggerEventMetadata() + require.NotNil(t, meta) + mu.Lock() + chunkLens = append(chunkLens, len(meta.WorkflowIds)) + mu.Unlock() + }).Return(nil).Maybe() + + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + for i := range nRegs { + publisher.Receive(ctx, newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[1], fmt.Sprintf("trigger_%d", i))) + waitForRegistration(t, underlying.registrationsCh) + } + + // 250 registrations at MaxBatchSize=100 → chunk lengths 100, 100, 50 per tick per peer. + require.Eventually(t, func() bool { + mu.Lock() + defer mu.Unlock() + var has100, has50 bool + var n100 int + for _, n := range chunkLens { + if n == 100 { + has100 = true + n100++ + } + if n == 50 { + has50 = true + } + } + return has100 && has50 && n100 >= 2 && len(chunkLens) >= 3 + }, 3*time.Second, 20*time.Millisecond) + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_UnregisterValidatesSenderMembership(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr := logger.Test(t) + + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + + peers := make([]rage.PeerID, 3) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + // peers[2] is a random peer not in any DON + peers[2] = rage.PeerID{0xff} + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + underlying := newMultiTrigger(capInfo) + dispatcher := mocks.NewDispatcher(t) + allowRegistrationChecks(dispatcher) + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + // Register a trigger from the valid workflow DON member + regEvent := newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[1], "triggerA") + publisher.Receive(ctx, regEvent) + waitForRegistration(t, underlying.registrationsCh) + + // Send unregister from a peer NOT in the workflow DON — should be ignored + unregMsg := &don2dontypes.MessageBody{ + Sender: peers[2][:], + Method: don2dontypes.MethodUnregisterTrigger, + CallerDonId: workflowDONID, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID1}, + TriggerIds: []string{"triggerA"}, + }, + }, + } + publisher.Receive(ctx, unregMsg) + + // UnregisterTrigger should NOT have been called on the underlying + select { + case trigID := <-underlying.unregisterCalled: + t.Fatalf("expected no unregister, but got unregister for %s", trigID) + default: + // expected: no unregister + } + + // Now send from the valid member — should succeed + unregMsg.Sender = peers[1][:] + publisher.Receive(ctx, unregMsg) + require.Equal(t, "triggerA", waitForUnregister(t, underlying.unregisterCalled)) + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_UnregisterRequiresQuorum(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr := logger.Test(t) + + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + + peers := make([]rage.PeerID, 4) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + require.NoError(t, peers[2].UnmarshalText([]byte(peerID3))) + require.NoError(t, peers[3].UnmarshalText([]byte("12D3KooWMoejJznyDuEk5aX6GvbjaG12UzeornPCBNzMRqdwrFJw"))) + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1], peers[2], peers[3]}, + F: 1, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + dispatcher := mocks.NewDispatcher(t) + allowRegistrationChecks(dispatcher) + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + + underlying := newMultiTrigger(capInfo) + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + for _, p := range []rage.PeerID{peers[1], peers[2], peers[3]} { + publisher.Receive(ctx, newRegisterTriggerMessageWithTriggerID(t, workflowDONID, p, "triggerA")) + } + require.Equal(t, "triggerA", waitForRegistration(t, underlying.registrationsCh).TriggerID) + + unregMeta := &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID1}, + TriggerIds: []string{"triggerA"}, + }, + } + recvUnreg := func(sender rage.PeerID) { + publisher.Receive(ctx, &don2dontypes.MessageBody{ + Sender: sender[:], + Method: don2dontypes.MethodUnregisterTrigger, + CallerDonId: workflowDONID, + Metadata: unregMeta, + }) + } + + recvUnreg(peers[1]) + recvUnreg(peers[2]) + select { + case id := <-underlying.unregisterCalled: + t.Fatalf("unregister after 2 of 3 nodes should not run; got %q", id) + default: + } + + recvUnreg(peers[3]) + require.Equal(t, "triggerA", waitForUnregister(t, underlying.unregisterCalled)) + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_UnregisterInvalidMetadata(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + _, publisher, _, peers := newServices(t, 1, 2, 1, time.Second) + + cases := []struct { + name string + meta *don2dontypes.MessageBody_TriggerEventMetadata + }{ + { + name: "nil metadata", + meta: nil, + }, + { + name: "empty workflow IDs", + meta: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{}, + TriggerIds: []string{"triggerA"}, + }, + }, + }, + { + name: "empty trigger IDs", + meta: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID1}, + TriggerIds: []string{}, + }, + }, + }, + { + name: "multiple workflow IDs", + meta: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID1, workflowID1}, + TriggerIds: []string{"triggerA"}, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + msg := &don2dontypes.MessageBody{ + Sender: peers[1][:], + Method: don2dontypes.MethodUnregisterTrigger, + CallerDonId: 2, + } + if tc.meta != nil { + msg.Metadata = tc.meta + } + // Should not panic + publisher.Receive(ctx, msg) + }) + } + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_AckCacheCleanup(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[0], peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + underlying := newMultiTrigger(capInfo) + dispatcher := mocks.NewDispatcher(t) + + triggerEventSent := make(chan rage.PeerID, 10) + dispatcher.On("Send", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + msg := args.Get(1).(*don2dontypes.MessageBody) + if msg.Method == don2dontypes.MethodTriggerEvent { + triggerEventSent <- args.Get(0).(rage.PeerID) + } + }).Return(nil).Maybe() + + // Very short MessageExpiry so ACK entries expire quickly + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 200 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 200 * time.Millisecond, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + + // Start before Receive so initMetrics() has run on registration and ACK paths. + require.NoError(t, publisher.Start(ctx)) + + regEvent := newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[0], "triggerA") + publisher.Receive(ctx, regEvent) + waitForRegistration(t, underlying.registrationsCh) + + ackMsg := newAckEventMessage(t, "event1", "triggerA", workflowDONID, peers[0]) + publisher.Receive(ctx, ackMsg) + + // cacheCleanupLoop ticks on MessageExpiry (200ms) and removes expired ack cache + // entries so a later trigger event is not suppressed. + + // Wait long enough for the ack cache entry to expire and be cleaned up + time.Sleep(500 * time.Millisecond) + + // Send a new trigger event for the same event ID and verify it gets sent to + // peers[0] again (not suppressed by the old ACK), proving the cleanup worked. + underlying.SendEvent("triggerA", commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ID: "event1"}, + }) + + sentTo := make(map[rage.PeerID]bool) + for range 2 { + select { + case peer := <-triggerEventSent: + sentTo[peer] = true + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for trigger event sends") + } + } + require.True(t, sentTo[peers[0]], "event should be re-sent to peers[0] after ack cache cleanup") + require.True(t, sentTo[peers[1]], "event should be sent to peers[1]") + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_SecondDeliveryAfterFullAck_ReachesAllPeers(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[0], peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + underlying := newMultiTrigger(capInfo) + dispatcher := mocks.NewDispatcher(t) + allowRegistrationChecks(dispatcher) + + var triggerEventMu sync.Mutex + triggerEventSendCount := 0 + dispatcher.On("Send", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + msg := args.Get(1).(*don2dontypes.MessageBody) + if msg.Method == don2dontypes.MethodTriggerEvent { + triggerEventMu.Lock() + triggerEventSendCount++ + triggerEventMu.Unlock() + } + }).Return(nil).Maybe() + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + regEvent := newRegisterTriggerMessageWithTriggerID(t, workflowDONID, peers[0], "triggerA") + publisher.Receive(ctx, regEvent) + waitForRegistration(t, underlying.registrationsCh) + + eventID := "shared-event-second-delivery-regression" + underlying.SendEvent("triggerA", commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ID: eventID}, + }) + + require.Eventually(t, func() bool { + triggerEventMu.Lock() + defer triggerEventMu.Unlock() + return triggerEventSendCount >= 2 + }, 2*time.Second, 10*time.Millisecond, "first delivery should emit MethodTriggerEvent once per workflow peer") + + triggerEventMu.Lock() + firstRoundTotal := triggerEventSendCount + triggerEventMu.Unlock() + require.Equal(t, 2, firstRoundTotal, "workflow DON has two members") + + publisher.Receive(ctx, newAckEventMessage(t, eventID, "triggerA", workflowDONID, peers[0])) + publisher.Receive(ctx, newAckEventMessage(t, eventID, "triggerA", workflowDONID, peers[1])) + + underlying.SendEvent("triggerA", commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ID: eventID}, + }) + + require.Eventually(t, func() bool { + triggerEventMu.Lock() + defer triggerEventMu.Unlock() + return triggerEventSendCount >= firstRoundTotal+2 + }, 2*time.Second, 10*time.Millisecond, + "second delivery with same triggerEventID must still fan out to all workflow peers (regression: ack-cache peer skip used to send zero)") + + triggerEventMu.Lock() + finalTotal := triggerEventSendCount + triggerEventMu.Unlock() + require.Equal(t, 4, finalTotal, "two delivery rounds × two workflow peers") + + require.NoError(t, publisher.Close()) +} + +func TestTriggerPublisher_RegisterTrigger_FailureShortCircuit(t *testing.T) { + t.Parallel() + + t.Run("user error suppresses retries", func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr := logger.Test(t) + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + } + userErr := caperrors.NewPublicUserError(errors.New("bad workflow config"), caperrors.InvalidArgument) + underlying := &errTrigger{info: capInfo, err: userErr} + + dispatcher := mocks.NewDispatcher(t) + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + regMsg := newRegisterTriggerMessage(t, workflowDONID, peers[1]) + publisher.Receive(ctx, regMsg) + require.Eventually(t, func() bool { return underlying.callCount.Load() == 1 }, 2*time.Second, 10*time.Millisecond) + + publisher.Receive(ctx, regMsg) + publisher.Receive(ctx, regMsg) + require.Eventually(t, func() bool { return underlying.callCount.Load() == 1 }, 2*time.Second, 10*time.Millisecond) + + require.NoError(t, publisher.Close()) + }) + + t.Run("system error allows retries", func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr := logger.Test(t) + capabilityDONID, workflowDONID := uint32(1), uint32(2) + + peers := make([]rage.PeerID, 2) + require.NoError(t, peers[0].UnmarshalText([]byte(peerID1))) + require.NoError(t, peers[1].UnmarshalText([]byte(peerID2))) + + capDonInfo := commoncap.DON{ + ID: capabilityDONID, + Members: []rage.PeerID{peers[0]}, + F: 0, + } + workflowDonInfo := commoncap.DON{ + ID: workflowDONID, + Members: []rage.PeerID{peers[1]}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + capInfo := commoncap.CapabilityInfo{ + ID: capID, + CapabilityType: commoncap.CapabilityTypeTrigger, + } + underlying := &errTrigger{info: capInfo, err: errors.New("transient system failure")} + + dispatcher := mocks.NewDispatcher(t) + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + publisher := don2don.NewTriggerPublisher(capInfo.ID, "", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(config, underlying, capDonInfo, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + + regMsg := newRegisterTriggerMessage(t, workflowDONID, peers[1]) + publisher.Receive(ctx, regMsg) + require.Eventually(t, func() bool { return underlying.callCount.Load() == 1 }, 2*time.Second, 10*time.Millisecond) + + require.Eventually(t, func() bool { + if underlying.callCount.Load() >= 2 { + return true + } + publisher.Receive(ctx, regMsg) + return underlying.callCount.Load() >= 2 + }, 2*time.Second, 10*time.Millisecond) + + require.Eventually(t, func() bool { + if underlying.callCount.Load() >= 3 { + return true + } + publisher.Receive(ctx, regMsg) + return underlying.callCount.Load() >= 3 + }, 2*time.Second, 10*time.Millisecond) + + require.NoError(t, publisher.Close()) + }) +} + +// errTrigger is a TriggerCapability that always returns an error from RegisterTrigger. +type errTrigger struct { + info commoncap.CapabilityInfo + err error + callCount atomic.Int32 +} + +func (tr *errTrigger) Info(_ context.Context) (commoncap.CapabilityInfo, error) { + return tr.info, nil +} + +func (tr *errTrigger) RegisterTrigger(_ context.Context, _ commoncap.TriggerRegistrationRequest) (<-chan commoncap.TriggerResponse, error) { + tr.callCount.Add(1) + return nil, tr.err +} + +func (tr *errTrigger) UnregisterTrigger(_ context.Context, _ commoncap.TriggerRegistrationRequest) error { + return nil +} + +func (tr *errTrigger) AckEvent(_ context.Context, _ string, _ string, _ string) error { + return nil +} + +func newRegisterTriggerMessageWithTriggerID(t *testing.T, callerDonID uint32, sender rage.PeerID, triggerID string) *don2dontypes.MessageBody { + triggerRequest := commoncap.TriggerRegistrationRequest{ + TriggerID: triggerID, + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + marshaled, err := pb.MarshalTriggerRegistrationRequest(triggerRequest) + require.NoError(t, err) + return &don2dontypes.MessageBody{ + Sender: sender[:], + Method: don2dontypes.MethodRegisterTrigger, + CallerDonId: callerDonID, + Payload: marshaled, + } +} + +// multiTrigger is a test trigger that supports multiple trigger registrations +// and can send events to specific triggers by triggerID +type multiTrigger struct { + info commoncap.CapabilityInfo + registrationsCh chan commoncap.TriggerRegistrationRequest + eventChans map[string]chan commoncap.TriggerResponse + unregisterCalled chan string + mu sync.Mutex +} + +func newMultiTrigger(info commoncap.CapabilityInfo) *multiTrigger { + return &multiTrigger{ + info: info, + registrationsCh: make(chan commoncap.TriggerRegistrationRequest, 128), + eventChans: make(map[string]chan commoncap.TriggerResponse), + unregisterCalled: make(chan string, 1), + } +} + +func (tr *multiTrigger) Info(_ context.Context) (commoncap.CapabilityInfo, error) { + return tr.info, nil +} + +func (tr *multiTrigger) AckEvent(_ context.Context, triggerID string, eventID string, method string) error { + return nil +} + +func (tr *multiTrigger) RegisterTrigger(_ context.Context, request commoncap.TriggerRegistrationRequest) (<-chan commoncap.TriggerResponse, error) { + tr.mu.Lock() + defer tr.mu.Unlock() + ch := make(chan commoncap.TriggerResponse, 10) + tr.eventChans[request.TriggerID] = ch + tr.registrationsCh <- request + return ch, nil +} + +func (tr *multiTrigger) UnregisterTrigger(_ context.Context, request commoncap.TriggerRegistrationRequest) error { + tr.mu.Lock() + defer tr.mu.Unlock() + tr.unregisterCalled <- request.TriggerID + if ch, ok := tr.eventChans[request.TriggerID]; ok { + close(ch) + delete(tr.eventChans, request.TriggerID) + } + return nil +} + +func (tr *multiTrigger) SendEvent(triggerID string, event commoncap.TriggerResponse) { + tr.mu.Lock() + defer tr.mu.Unlock() + if ch, ok := tr.eventChans[triggerID]; ok { + ch <- event + } +} diff --git a/libs/x/don2don/trigger_registration_load_test.go b/libs/x/don2don/trigger_registration_load_test.go new file mode 100644 index 000000000..cf8f5b23c --- /dev/null +++ b/libs/x/don2don/trigger_registration_load_test.go @@ -0,0 +1,758 @@ +package don2don_test + +import ( + "context" + "fmt" + "sort" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + "github.com/smartcontractkit/capabilities/libs/x/don2don/aggregation" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + "github.com/smartcontractkit/capabilities/libs/x/internal/synctest" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// donFaultTolerance is the DON F value (uint8) used in load tests; v must be in [0, 255]. +func donFaultTolerance(t *testing.T, v int) uint8 { + t.Helper() + require.GreaterOrEqual(t, v, 0) + require.LessOrEqual(t, v, 255, "F value out of uint8 range for test DON") + return uint8(v) //nolint:gosec // G115: range-checked above +} + +// countingDispatcher is a types.Dispatcher implementation that atomically +// counts Send calls by method type. +type countingDispatcher struct { + services.StateMachine + mu sync.RWMutex + counts map[string]*atomic.Int64 +} + +func newCountingDispatcher() *countingDispatcher { + return &countingDispatcher{ + counts: make(map[string]*atomic.Int64), + } +} + +func (d *countingDispatcher) Send(_ rage.PeerID, msgBody *don2dontypes.MessageBody) error { + method := msgBody.Method + d.mu.RLock() + c, ok := d.counts[method] + d.mu.RUnlock() + if !ok { + d.mu.Lock() + c, ok = d.counts[method] + if !ok { + c = &atomic.Int64{} + d.counts[method] = c + } + d.mu.Unlock() + } + c.Add(1) + return nil +} + +func (d *countingDispatcher) Count(method string) int64 { + d.mu.RLock() + defer d.mu.RUnlock() + if c, ok := d.counts[method]; ok { + return c.Load() + } + return 0 +} + +func (d *countingDispatcher) Reset() { + d.mu.Lock() + defer d.mu.Unlock() + d.counts = make(map[string]*atomic.Int64) +} + +func (d *countingDispatcher) Snapshot() map[string]int64 { + d.mu.RLock() + defer d.mu.RUnlock() + snap := make(map[string]int64, len(d.counts)) + for method, c := range d.counts { + snap[method] = c.Load() + } + return snap +} + +func (d *countingDispatcher) SetReceiver(_ string, _ uint32, _ don2dontypes.Receiver) error { + return nil +} +func (d *countingDispatcher) RemoveReceiver(_ string, _ uint32) {} +func (d *countingDispatcher) SetReceiverForMethod(_ string, _ uint32, _ string, _ don2dontypes.Receiver) error { + return nil +} +func (d *countingDispatcher) RemoveReceiverForMethod(_ string, _ uint32, _ string) {} +func (d *countingDispatcher) Start(_ context.Context) error { return nil } +func (d *countingDispatcher) Close() error { return nil } +func (d *countingDispatcher) Ready() error { return nil } +func (d *countingDispatcher) HealthReport() map[string]error { return nil } +func (d *countingDispatcher) Name() string { return "countingDispatcher" } + +// formatP2PSnapshot returns a stable, human-readable summary of outbound P2P +// sends by method (for test logs / team evidence). +func formatP2PSnapshot(snap map[string]int64) string { + if len(snap) == 0 { + return "(none)" + } + methods := make([]string, 0, len(snap)) + for m := range snap { + methods = append(methods, m) + } + sort.Strings(methods) + var b strings.Builder + for _, m := range methods { + fmt.Fprintf(&b, "%s=%d ", m, snap[m]) + } + return strings.TrimSpace(b.String()) +} + +// --- helpers --- + +func generatePeers(t *testing.T, n int) []rage.PeerID { + t.Helper() + peers := make([]rage.PeerID, n) + for i := range n { + pid := rage.MustNewPeerID() + require.NoError(t, peers[i].UnmarshalText([]byte(pid))) + } + return peers +} + +func generateWorkflowID(i int) string { + return fmt.Sprintf("%064x", i) +} + +// noopTrigger implements commoncap.TriggerCapability with no-op operations +// and large buffered channels so it never blocks during load tests. +type noopTrigger struct { + info commoncap.CapabilityInfo +} + +func (t *noopTrigger) Info(_ context.Context) (commoncap.CapabilityInfo, error) { + return t.info, nil +} + +func (t *noopTrigger) RegisterTrigger(_ context.Context, _ commoncap.TriggerRegistrationRequest) (<-chan commoncap.TriggerResponse, error) { + ch := make(chan commoncap.TriggerResponse, 1) + return ch, nil +} + +func (t *noopTrigger) UnregisterTrigger(_ context.Context, _ commoncap.TriggerRegistrationRequest) error { + return nil +} + +func (t *noopTrigger) AckEvent(_ context.Context, _ string, _ string, _ string) error { + return nil +} + +// --- Test: Subscriber registrationLoop traffic volume --- + +func TestRegistrationTrafficVolume(t *testing.T) { + t.Parallel() + + cases := []struct { + nRegistrations int + capDonSize int + workflowDonSize int + }{ + {100, 4, 7}, + {500, 4, 7}, + {1000, 4, 7}, + {2500, 4, 7}, + {5000, 4, 7}, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("N=%d_cap=%d_wf=%d", tc.nRegistrations, tc.capDonSize, tc.workflowDonSize), func(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + dispatcher := newCountingDispatcher() + + capDonMembers := generatePeers(t, tc.capDonSize) + capDon := commoncap.DON{ID: 1, Members: capDonMembers, F: donFaultTolerance(t, tc.capDonSize/3)} + + workflowDonMembers := generatePeers(t, tc.workflowDonSize) + workflowDon := commoncap.DON{ID: 2, Members: workflowDonMembers, F: donFaultTolerance(t, (tc.workflowDonSize-1)/3)} + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1", + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Load Test Trigger", + } + + cfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 500 * time.Millisecond, + RegistrationExpiry: 2 * time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Hour, + } + + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "LogTrigger", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(cfg.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(cfg, capInfo, workflowDon.ID, capDon, agg)) + + // Register all triggers before Start — RegisterTrigger does not + // send to the cap DON until registrationLoop runs after Start. + for i := range tc.nRegistrations { + req := commoncap.TriggerRegistrationRequest{ + TriggerID: fmt.Sprintf("trigger_%d", i), + Metadata: commoncap.RequestMetadata{WorkflowID: generateWorkflowID(i)}, + } + _, err := subscriber.RegisterTrigger(t.Context(), req) + require.NoError(t, err) + } + + // Reset counters after initial registration sends, then start + // the loop so the first tick generates a clean measurement. + dispatcher.Reset() + require.NoError(t, subscriber.Start(t.Context())) + t.Cleanup(func() { subscriber.Close() }) + + // Wait for exactly one tick (500ms refresh, wait 700ms to give time) + expectedSends := int64(tc.nRegistrations * tc.capDonSize) + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + if dispatcher.Count(don2dontypes.MethodRegisterTrigger) >= expectedSends { + break + } + time.Sleep(10 * time.Millisecond) + } + + snap := dispatcher.Snapshot() + registerCount := snap[don2dontypes.MethodRegisterTrigger] + + t.Logf("Registrations: %d, CapDON peers: %d", tc.nRegistrations, tc.capDonSize) + t.Logf("Expected P2P sends per tick: %d, Actual: %d", expectedSends, registerCount) + t.Logf("Full snapshot: %v", snap) + + // At least one tick's worth of sends must have occurred. + require.GreaterOrEqual(t, registerCount, expectedSends, + "subscriber registrationLoop should send at least N*capDonMembers RegisterTrigger messages per tick") + // Verify it's a multiple of expectedSends (each tick sends exactly this many). + require.Equal(t, int64(0), registerCount%expectedSends, + "total sends should be an exact multiple of per-tick sends") + }) + } +} + +// --- Test: Publisher sendRegistrationChecks traffic volume --- + +func TestRegistrationCheckTrafficVolume(t *testing.T) { + t.Parallel() + cases := []struct { + nRegistrations int + capDonSize int + workflowDonSize int + }{ + {100, 4, 7}, + {500, 4, 7}, + {1000, 4, 7}, + {2500, 4, 7}, + {5000, 4, 7}, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("N=%d", tc.nRegistrations), func(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + ctx := t.Context() + lggr := logger.Test(t) + dispatcher := newCountingDispatcher() + + capDonMembers := generatePeers(t, tc.capDonSize) + capDon := commoncap.DON{ID: 1, Members: capDonMembers, F: donFaultTolerance(t, tc.capDonSize/3)} + + workflowDonMembers := generatePeers(t, tc.workflowDonSize) + workflowDon := commoncap.DON{ID: 2, Members: workflowDonMembers, F: donFaultTolerance(t, (tc.workflowDonSize-1)/3)} + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1", + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Load Test Trigger", + } + + underlying := &noopTrigger{info: capInfo} + + // Use the same refresh interval before and after registration so + // registrationCheckLoop's ticker matches the measurement window (a + // long initial interval would not pick up a later shorter interval + // until the next tick of the old ticker). + cfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 50 * time.Millisecond, + RegistrationExpiry: 2 * time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Hour, + MaxBatchSize: 100, + BatchCollectionPeriod: time.Second, + } + + workflowDONs := map[uint32]commoncap.DON{workflowDon.ID: workflowDon} + publisher := don2don.NewTriggerPublisher(capInfo.ID, "LogTrigger", dispatcher, lggr) + require.NoError(t, publisher.SetConfig(cfg, underlying, capDon, workflowDONs)) + require.NoError(t, publisher.Start(ctx)) + t.Cleanup(func() { require.NoError(t, publisher.Close()) }) + + // Register N triggers by feeding MethodRegisterTrigger messages. + // With F=0 on the workflow DON side used in newServices patterns, + // a single sender suffices for quorum. Here F>0 so we need 2F+1 + // senders. Use all workflow DON members. + for i := range tc.nRegistrations { + triggerID := fmt.Sprintf("trigger_%d", i) + wfID := generateWorkflowID(i) + + for _, sender := range workflowDonMembers { + req := commoncap.TriggerRegistrationRequest{ + TriggerID: triggerID, + Metadata: commoncap.RequestMetadata{WorkflowID: wfID}, + } + marshaled, err := pb.MarshalTriggerRegistrationRequest(req) + require.NoError(t, err) + + msg := &don2dontypes.MessageBody{ + Sender: sender[:], + Method: don2dontypes.MethodRegisterTrigger, + CallerDonId: workflowDon.ID, + Payload: marshaled, + } + publisher.Receive(ctx, msg) + } + } + + dispatcher.Reset() + + // Count registration checks after registrations are in place (ignore + // any check traffic during the registration burst above). + time.Sleep(200 * time.Millisecond) + + snap := dispatcher.Snapshot() + checkCount := snap[don2dontypes.MethodTriggerRegistrationCheck] + + // With MaxBatchSize=100, each registration-check tick sends ceil(N/100) messages per + // workflow-DON peer (chunked metadata), not one giant payload. + chunksPerTick := (tc.nRegistrations + 99) / 100 + minPerFullTick := int64(chunksPerTick * tc.workflowDonSize) + + t.Logf("Registrations: %d, WorkflowDON peers: %d, chunksPerTick: %d", tc.nRegistrations, tc.workflowDonSize, chunksPerTick) + t.Logf("Expected min (>= one tick): %d, Actual: %d", minPerFullTick, checkCount) + t.Logf("Full snapshot: %v", snap) + + // Allow multiple ticks in the 200ms window (50ms refresh → several ticks). + require.GreaterOrEqual(t, checkCount, minPerFullTick, + "publisher should send at least chunksPerTick*peers TriggerRegistrationCheck messages per tick") + // Total sends stay far below one message per registration (worst case would be N×peers). + require.Less(t, checkCount, int64(tc.nRegistrations*tc.workflowDonSize), + "registration check traffic must remain far below per-registration fan-out") + }) + }) + } +} + +// --- Benchmark: Publisher processing rate for duplicate registrations --- + +func BenchmarkRegistrationProcessing(b *testing.B) { + ctx := context.Background() + lggr, _ := logger.New() + dispatcher := newCountingDispatcher() + + capDonMembers := make([]rage.PeerID, 4) + for i := range capDonMembers { + pid := rage.MustNewPeerID() + if err := capDonMembers[i].UnmarshalText([]byte(pid)); err != nil { + b.Fatal(err) + } + } + capDon := commoncap.DON{ID: 1, Members: capDonMembers, F: 1} + + workflowDonMembers := make([]rage.PeerID, 7) + for i := range workflowDonMembers { + pid := rage.MustNewPeerID() + if err := workflowDonMembers[i].UnmarshalText([]byte(pid)); err != nil { + b.Fatal(err) + } + } + workflowDon := commoncap.DON{ID: 2, Members: workflowDonMembers, F: 2} + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1", + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Benchmark Trigger", + } + underlying := &noopTrigger{info: capInfo} + + cfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: time.Hour, + RegistrationExpiry: 2 * time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Hour, + MaxBatchSize: 100, + BatchCollectionPeriod: time.Second, + } + workflowDONs := map[uint32]commoncap.DON{workflowDon.ID: workflowDon} + publisher := don2don.NewTriggerPublisher(capInfo.ID, "LogTrigger", dispatcher, lggr) + if err := publisher.SetConfig(cfg, underlying, capDon, workflowDONs); err != nil { + b.Fatal(err) + } + if err := publisher.Start(ctx); err != nil { + b.Fatal(err) + } + b.Cleanup(func() { _ = publisher.Close() }) + + // Pre-register one trigger so subsequent Receive calls hit the fast + // "already exists" path. + sender := workflowDonMembers[0] + triggerReq := commoncap.TriggerRegistrationRequest{ + TriggerID: "trigger_0", + Metadata: commoncap.RequestMetadata{WorkflowID: generateWorkflowID(0)}, + } + marshaled, err := pb.MarshalTriggerRegistrationRequest(triggerReq) + if err != nil { + b.Fatal(err) + } + regMsg := &don2dontypes.MessageBody{ + Sender: sender[:], + Method: don2dontypes.MethodRegisterTrigger, + CallerDonId: workflowDon.ID, + Payload: marshaled, + } + publisher.Receive(ctx, regMsg) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + publisher.Receive(ctx, regMsg) + } + + b.StopTimer() + elapsed := b.Elapsed() + rate := float64(b.N) / elapsed.Seconds() + b.ReportMetric(rate, "msgs/sec") +} + +// --- Test: Subscriber RLock hold duration during registration loop --- + +func TestRegistrationLoopLockDuration(t *testing.T) { + t.Parallel() + + cases := []int{100, 500, 1000, 2500} + + for _, n := range cases { + t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + dispatcher := newCountingDispatcher() + + capDonMembers := generatePeers(t, 4) + capDon := commoncap.DON{ID: 1, Members: capDonMembers, F: 1} + + workflowDonMembers := generatePeers(t, 7) + workflowDon := commoncap.DON{ID: 2, Members: workflowDonMembers, F: 2} + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1", + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Lock Duration Test", + } + + cfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 50 * time.Millisecond, + RegistrationExpiry: 2 * time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Hour, + } + + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "LogTrigger", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(cfg.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(cfg, capInfo, workflowDon.ID, capDon, agg)) + + for i := range n { + req := commoncap.TriggerRegistrationRequest{ + TriggerID: fmt.Sprintf("trigger_%d", i), + Metadata: commoncap.RequestMetadata{WorkflowID: generateWorkflowID(i)}, + } + _, err := subscriber.RegisterTrigger(t.Context(), req) + require.NoError(t, err) + } + + dispatcher.Reset() + + expectedSends := int64(n * 4) // n registrations * 4 cap DON members + + require.NoError(t, subscriber.Start(t.Context())) + t.Cleanup(func() { subscriber.Close() }) + + start := time.Now() + + // Wait for the tick to complete — poll until we see the expected sends. + deadline := start.Add(10 * time.Second) + for time.Now().Before(deadline) { + if dispatcher.Count(don2dontypes.MethodRegisterTrigger) >= expectedSends { + break + } + time.Sleep(5 * time.Millisecond) + } + elapsed := time.Since(start) + + actual := dispatcher.Count(don2dontypes.MethodRegisterTrigger) + require.GreaterOrEqual(t, actual, expectedSends, "should have sent all registrations") + + t.Logf("N=%d: %d P2P sends completed in %v (%.0f sends/sec)", + n, actual, elapsed, float64(actual)/elapsed.Seconds()) + + // Sanity: with a noop dispatcher, even 10,000 sends should + // complete in well under 1 second. + require.Less(t, elapsed, 5*time.Second, + "registration loop tick should complete in reasonable time") + }) + } +} + +// TestTrafficAttribution_RegisterLoopVsChecksVsEventsAndAcks documents where +// outbound P2P traffic is generated: subscriber registrationLoop vs publisher +// registration checks vs one trigger event dispatch vs subscriber ACK fan-out. +// It uses the same topology as staging analysis (4 cap DON peers, 7 workflow +// DON peers, workflow F=2 → quorum 5). This does not simulate dispatcher drops +// or shared-channel saturation; see test log for that limitation. +func TestTrafficAttribution_RegisterLoopVsChecksVsEventsAndAcks(t *testing.T) { + t.Parallel() + ctx := t.Context() + lggr := logger.Test(t) + + const ( + nRegistrations = 120 // subscriber registrationLoop load (keep moderate for CI log volume) + nPublisherRegs = 80 // publisher-side registrations (quorum cost only) + capDonPeerCount = 4 + wfDonPeerCount = 7 + wfDonFaultTolerance = 2 // minRequired = 2*F+1 = 5 for reg aggregation and ACK quorum + ) + + capPeers := generatePeers(t, capDonPeerCount) + capDon := commoncap.DON{ID: 1, Members: capPeers, F: 1} + + wfPeers := generatePeers(t, wfDonPeerCount) + wfDon := commoncap.DON{ID: 2, Members: wfPeers, F: donFaultTolerance(t, wfDonFaultTolerance)} + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1", + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Traffic attribution", + } + + // --- Phase 1: subscriber — one registrationLoop tick (periodic refresh) --- + subRegDisp := newCountingDispatcher() + subCfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 500 * time.Millisecond, + RegistrationExpiry: 2 * time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Hour, + } + sub := don2don.NewTriggerSubscriber(capInfo.ID, "LogTrigger", subRegDisp, lggr) + agg := aggregation.NewDefaultModeAggregator(subCfg.MinResponsesToAggregate) + require.NoError(t, sub.SetConfig(subCfg, capInfo, wfDon.ID, capDon, agg)) + for i := range nRegistrations { + req := commoncap.TriggerRegistrationRequest{ + TriggerID: fmt.Sprintf("trigger_%d", i), + Metadata: commoncap.RequestMetadata{WorkflowID: generateWorkflowID(i)}, + } + _, err := sub.RegisterTrigger(ctx, req) + require.NoError(t, err) + } + subRegDisp.Reset() + require.NoError(t, sub.Start(ctx)) + t.Cleanup(func() { require.NoError(t, sub.Close()) }) + + perTickReg := int64(nRegistrations * capDonPeerCount) + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + if subRegDisp.Count(don2dontypes.MethodRegisterTrigger) >= perTickReg { + break + } + time.Sleep(10 * time.Millisecond) + } + phase1Reg := subRegDisp.Count(don2dontypes.MethodRegisterTrigger) + require.GreaterOrEqual(t, phase1Reg, perTickReg) + require.Equal(t, int64(0), phase1Reg%perTickReg) + + // --- Phase 2: subscriber — one engine round-trip: deliver event + AckEvent (ACK fan-out to cap DON) --- + subAckDisp := newCountingDispatcher() + sub2 := don2don.NewTriggerSubscriber(capInfo.ID, "LogTrigger", subAckDisp, lggr) + require.NoError(t, sub2.SetConfig(subCfg, capInfo, wfDon.ID, capDon, agg)) + require.NoError(t, sub2.Start(ctx)) + t.Cleanup(func() { require.NoError(t, sub2.Close()) }) + + const soloTrig = "solo-trigger" + regReq := commoncap.TriggerRegistrationRequest{ + TriggerID: soloTrig, + Metadata: commoncap.RequestMetadata{WorkflowID: workflowID1}, + } + _, err := sub2.RegisterTrigger(ctx, regReq) + require.NoError(t, err) + subAckDisp.Reset() + + ev := buildTriggerEventWithTriggerID(t, capPeers[0][:], workflowID1, soloTrig, "event-attr-1") + sub2.Receive(ctx, ev) + require.NoError(t, sub2.AckEvent(ctx, soloTrig, "event-attr-1", "LogTrigger")) + + ackFanout := subAckDisp.Count(don2dontypes.MethodTriggerEventAck) + require.Equal(t, int64(capDonPeerCount), ackFanout, + "subscriber AckEvent should send one TriggerEventAck to each cap DON peer") + + // --- Phase 3: publisher — registration checks over a short window --- + // Use noopTrigger here: multiTrigger's registrationsCh (buffer 10) fills and + // blocks RegisterTrigger after 10 registrations if not drained. + pubDisp := newCountingDispatcher() + underlyingNoop := &noopTrigger{info: capInfo} + pubCfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 50 * time.Millisecond, + RegistrationExpiry: 2 * time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Hour, + MaxBatchSize: 100, + BatchCollectionPeriod: time.Second, + } + wfDONs := map[uint32]commoncap.DON{wfDon.ID: wfDon} + pub := don2don.NewTriggerPublisher(capInfo.ID, "LogTrigger", pubDisp, lggr) + require.NoError(t, pub.SetConfig(pubCfg, underlyingNoop, capDon, wfDONs)) + require.NoError(t, pub.Start(ctx)) + t.Cleanup(func() { require.NoError(t, pub.Close()) }) + + minRegSenders := 2*int(wfDon.F) + 1 + for i := range nPublisherRegs { + tid := fmt.Sprintf("pub_trig_%d", i) + wid := generateWorkflowID(10000 + i) + for j := range minRegSenders { + req := commoncap.TriggerRegistrationRequest{ + TriggerID: tid, + Metadata: commoncap.RequestMetadata{WorkflowID: wid}, + } + payload, mErr := pb.MarshalTriggerRegistrationRequest(req) + require.NoError(t, mErr) + pub.Receive(ctx, &don2dontypes.MessageBody{ + Sender: wfPeers[j][:], + Method: don2dontypes.MethodRegisterTrigger, + CallerDonId: wfDon.ID, + Payload: payload, + }) + } + } + + pubDisp.Reset() + + time.Sleep(200 * time.Millisecond) + chunksPerPublisherTick := (nPublisherRegs + int(pubCfg.MaxBatchSize) - 1) / int(pubCfg.MaxBatchSize) + pubCheckSendsPerTick := int64(chunksPerPublisherTick * wfDonPeerCount) + + phase3Checks := pubDisp.Count(don2dontypes.MethodTriggerRegistrationCheck) + require.GreaterOrEqual(t, phase3Checks, pubCheckSendsPerTick, + "expect at least one registration-check tick (chunked by MaxBatchSize)") + // Measurement spans multiple RegistrationRefresh ticks — total sends grow with ticks, + // but stay far below naive per-registration × peers fan-out. + require.Less(t, phase3Checks, int64(nPublisherRegs*wfDonPeerCount), + "registration check traffic must stay below per-registration × workflow peers") + + // --- Phase 4: publisher — one underlying trigger event + workflow ACK quorum --- + pubEvDisp := newCountingDispatcher() + underEv := newMultiTrigger(capInfo) + pub2 := don2don.NewTriggerPublisher(capInfo.ID, "LogTrigger", pubEvDisp, lggr) + evCfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: time.Hour, + RegistrationExpiry: 2 * time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Hour, + // MaxBatchSize must be 1 (or BatchCollectionPeriod < 10ms) so trigger-event batching + // is disabled; otherwise events wait for the batch window and this phase sees 0 sends. + MaxBatchSize: 1, + BatchCollectionPeriod: time.Second, + } + require.NoError(t, pub2.SetConfig(evCfg, underEv, capDon, wfDONs)) + require.NoError(t, pub2.Start(ctx)) + t.Cleanup(func() { require.NoError(t, pub2.Close()) }) + + const evTrig = "event-path-trigger" + evWid := workflowID1 + for j := range minRegSenders { + req := commoncap.TriggerRegistrationRequest{ + TriggerID: evTrig, + Metadata: commoncap.RequestMetadata{WorkflowID: evWid}, + } + payload, mErr := pb.MarshalTriggerRegistrationRequest(req) + require.NoError(t, mErr) + pub2.Receive(ctx, &don2dontypes.MessageBody{ + Sender: wfPeers[j][:], + Method: don2dontypes.MethodRegisterTrigger, + CallerDonId: wfDon.ID, + Payload: payload, + }) + } + <-underEv.registrationsCh // wait for publisher to register underlying trigger + + pubEvDisp.Reset() + underEv.SendEvent(evTrig, commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ID: "publisher-ev-1"}, + }) + time.Sleep(50 * time.Millisecond) + afterEvent := pubEvDisp.Count(don2dontypes.MethodTriggerEvent) + require.Equal(t, int64(wfDonPeerCount), afterEvent, + "one non-batched trigger event should be dispatched to each workflow DON peer") + + beforeAck := pubEvDisp.Snapshot() + for j := range minRegSenders { + pub2.Receive(ctx, newAckEventMessage(t, "publisher-ev-1", evTrig, wfDon.ID, wfPeers[j])) + } + time.Sleep(20 * time.Millisecond) + afterAck := pubEvDisp.Snapshot() + + // ACK handling does not add outbound P2P sends on the publisher path. + for m, v := range afterAck { + if m == don2dontypes.MethodTriggerEvent { + require.Equal(t, beforeAck[m], v, "ACK quorum should not cause extra trigger event sends") + } + } + + // --- Evidence summary for staging / team discussion --- + t.Logf("=== P2P traffic attribution (same topology: %d cap peers, %d wf peers, F=%d) ===", + capDonPeerCount, wfDonPeerCount, wfDonFaultTolerance) + t.Logf("Phase 1 — subscriber registrationLoop (one tick, N=%d): RegisterTrigger total=%d (expected per tick: %d = N×capPeers)", + nRegistrations, phase1Reg, perTickReg) + t.Logf("Phase 2 — subscriber AckEvent after one event: TriggerEventAck total=%d (expected: %d = capPeers)", + ackFanout, capDonPeerCount) + t.Logf("Phase 3 — publisher sendRegistrationChecks (~200ms, N=%d, MaxBatchSize=%d): TriggerRegistrationCheck total=%d (min one tick ≈ %d = ceil(N/batch)×wfPeers; window spans multiple ticks)", + nPublisherRegs, pubCfg.MaxBatchSize, phase3Checks, pubCheckSendsPerTick) + t.Logf("Phase 4 — publisher one event then %d ACKs: TriggerEvent sends=%d (expected: %d = wfPeers); snapshot after ACK: %s", + minRegSenders, afterEvent, wfDonPeerCount, formatP2PSnapshot(afterAck)) + + // Compare single-tick orders of magnitude: phase1 is one subscriber refresh tick; + // phase3 denominator is one publisher registration-check tick (chunked), not the + // summed multi-tick window total (phase3Checks). + regToCheckRatio := float64(perTickReg) / float64(pubCheckSendsPerTick) + t.Logf("--- Ratios (illustrative; phase windows differ) ---") + t.Logf("registrationLoop RegisterTrigger (one tick) / publisher registration-check sends per tick ≈ %.1fx", + regToCheckRatio) + ackDenom := max(ackFanout, 1) + t.Logf("registrationLoop per tick / one subscriber AckEvent round ≈ %.1fx", + float64(perTickReg)/float64(ackDenom)) + evDenom := max(afterEvent, 1) + t.Logf("registrationLoop per tick / one trigger event dispatch ≈ %.1fx", + float64(perTickReg)/float64(evDenom)) + + require.Greater(t, regToCheckRatio, 10.0, + "evidence: periodic registration refresh traffic should dwarf registration-check traffic") + + t.Logf("NOTE: this test uses instant no-op dispatchers (no drops). To evidence drop/retry " + + "amplification, add a bounded-queue or failing Send mock in a separate test.") +} diff --git a/libs/x/don2don/trigger_subscriber.go b/libs/x/don2don/trigger_subscriber.go new file mode 100644 index 000000000..0b9c8744b --- /dev/null +++ b/libs/x/don2don/trigger_subscriber.go @@ -0,0 +1,540 @@ +package don2don + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/libs/x/don2don/messagecache" + "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// TriggerSubscriber is a shim for remote trigger capabilities. +// It translates between capability API calls and network messages. +// Its responsibilities are: +// 1. Periodically refresh all registrations for remote triggers. +// 2. Collect trigger events from remote nodes and aggregate responses via a customizable aggregator. +// +// TriggerSubscriber communicates with corresponding TriggerReceivers on remote nodes. +type triggerSubscriber struct { + capabilityID string + capMethodName string + dispatcher types.Dispatcher + cfg atomic.Pointer[dynamicConfig] + messageCache *messagecache.MessageCache[triggerEventKey, rage.PeerID] + // In theory we could identify all trigger registrations only by TriggerID (Workflow Engine + // already includes WorkflowID inside TriggerID). However, keeping the workflowID has some benefits: + // - Protection against changes to Engine's logic. + // - Better logging and debugging. + // - Easier migration. + // workflowID -> triggerID -> subRegState + registeredWorkflows map[string]map[string]*subRegState + // Records that the workflow engine already issued ACK fan-out for this logical event + // (registration TriggerID + TriggerEventId). Used to replay ACK on duplicate Receive without + // re-aggregating or delivering to the engine again. + ackReplayCache map[ackReplayKey]int64 + mu sync.RWMutex // protects registeredWorkflows, messageCache, and ackReplayCache + stopCh services.StopChan + wg sync.WaitGroup + lggr logger.Logger +} + +type dynamicConfig struct { + remoteConfig *commoncap.RemoteTriggerConfig + capInfo commoncap.CapabilityInfo + capDonInfo commoncap.DON + capDonMembers map[rage.PeerID]struct{} + localDonID uint32 + aggregator types.Aggregator +} + +type triggerEventKey struct { + triggerEventID string + workflowID string + triggerID string +} + +type ackReplayKey struct { + triggerID string + triggerEventID string +} + +type subRegState struct { + callback chan commoncap.TriggerResponse + rawRequest []byte +} + +type TriggerSubscriber interface { + commoncap.TriggerCapability + Receive(ctx context.Context, msg *types.MessageBody) + SetConfig(config *commoncap.RemoteTriggerConfig, capInfo commoncap.CapabilityInfo, localDONID uint32, remoteDON commoncap.DON, aggregator types.Aggregator) error +} + +var _ commoncap.TriggerCapability = &triggerSubscriber{} +var _ types.Receiver = &triggerSubscriber{} +var _ services.Service = &triggerSubscriber{} + +const ( + // Engine reads trigger events without blocking and applies its own limits + sendChannelBufferSize = 1000 + maxBatchedWorkflowIDs = 1000 +) + +func NewTriggerSubscriber(capabilityID string, capMethodName string, dispatcher types.Dispatcher, lggr logger.Logger) *triggerSubscriber { + return &triggerSubscriber{ + capabilityID: capabilityID, + capMethodName: capMethodName, + dispatcher: dispatcher, + messageCache: messagecache.NewMessageCache[triggerEventKey, rage.PeerID](), + ackReplayCache: make(map[ackReplayKey]int64), + registeredWorkflows: make(map[string]map[string]*subRegState), + stopCh: make(services.StopChan), + lggr: logger.With(logger.Named(lggr, "TriggerSubscriber"), "capabilityID", capabilityID, "capMethodName", capMethodName), + } +} + +func (s *triggerSubscriber) Start(ctx context.Context) error { + s.mu.RLock() + defer s.mu.RUnlock() + cfg := s.cfg.Load() + + // Validate that all required fields are set before starting + if cfg == nil { + return errors.New("config not set - call SetConfig() before Start()") + } + if cfg.remoteConfig == nil { + return errors.New("remoteConfig not set - call SetConfig() before Start()") + } + if cfg.capInfo.ID == "" { + return errors.New("capability info not set - call SetConfig() before Start()") + } + if cfg.localDonID == 0 { + return errors.New("local DON ID not set - call SetConfig() before Start()") + } + if len(cfg.capDonInfo.Members) == 0 { + return errors.New("capability DON info not set - call SetConfig() before Start()") + } + if cfg.aggregator == nil { + return errors.New("aggregator not set - call SetAggregator() before Start()") + } + if s.dispatcher == nil { + return errors.New("dispatcher set to nil, cannot start triggerSubscriber") + } + + s.wg.Add(2) + go s.registrationLoop() + go s.eventCleanupLoop() + s.lggr.Info("TriggerSubscriber started") + return nil +} + +func (s *triggerSubscriber) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + cfg := s.cfg.Load() + if cfg == nil { + return commoncap.CapabilityInfo{}, errors.New("config not set - call SetConfig() before Info()") + } + return cfg.capInfo, nil +} + +func (s *triggerSubscriber) AckEvent(ctx context.Context, triggerID string, eventID string, method string) error { + s.lggr.Debugw("AckEvent called on subscriber", "triggerID", triggerID, "eventID", eventID) + cfg := s.cfg.Load() + for _, peerID := range cfg.capDonInfo.Members { + m := &types.MessageBody{ + CapabilityId: cfg.capInfo.ID, + CapabilityDonId: cfg.capDonInfo.ID, + CallerDonId: cfg.localDonID, + Method: types.MethodTriggerEventAck, + CapabilityMethod: s.capMethodName, + Metadata: &types.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &types.TriggerEventMetadata{ + TriggerEventId: eventID, + TriggerIds: []string{triggerID}, // triggerID contains workflowID + }, + }, + } + err := s.dispatcher.Send(peerID, m) + if err != nil { + s.lggr.Errorw("failed to send message", "donId", cfg.capDonInfo.ID, "peerId", peerID, "err", err) + } + } + rk := ackReplayKey{triggerID: triggerID, triggerEventID: eventID} + s.mu.Lock() + s.ackReplayCache[rk] = time.Now().UnixMilli() + s.mu.Unlock() + return nil +} + +func (s *triggerSubscriber) RegisterTrigger(ctx context.Context, request commoncap.TriggerRegistrationRequest) (<-chan commoncap.TriggerResponse, error) { + rawRequest, err := pb.MarshalTriggerRegistrationRequest(request) + if err != nil { + return nil, err + } + if request.Metadata.WorkflowID == "" { + return nil, errors.New("empty workflowID") + } + + cfg := s.cfg.Load() + if cfg == nil { + return nil, errors.New("config not set - call SetConfig() first") + } + + s.mu.Lock() + defer s.mu.Unlock() + s.lggr.Infow("RegisterTrigger called", "donId", cfg.capDonInfo.ID, "workflowID", request.Metadata.WorkflowID, "triggerID", request.TriggerID) + triggerMap, ok := s.registeredWorkflows[request.Metadata.WorkflowID] + if !ok { + triggerMap = make(map[string]*subRegState) + s.registeredWorkflows[request.Metadata.WorkflowID] = triggerMap + } + regState, ok := triggerMap[request.TriggerID] + if !ok { + regState = &subRegState{ + callback: make(chan commoncap.TriggerResponse, sendChannelBufferSize), + rawRequest: rawRequest, + } + triggerMap[request.TriggerID] = regState + } else { + regState.rawRequest = rawRequest + s.lggr.Warnw("RegisterTrigger re-registering trigger", "donId", cfg.capDonInfo.ID, "workflowID", request.Metadata.WorkflowID, "triggerID", request.TriggerID) + } + + return regState.callback, nil +} + +func (s *triggerSubscriber) registrationLoop() { + defer s.wg.Done() + cfg := s.cfg.Load() + tickerDuration := cfg.remoteConfig.RegistrationRefresh + ticker := time.NewTicker(tickerDuration) + defer ticker.Stop() + for { + select { + case <-s.stopCh: + return + case <-ticker.C: + cfg := s.cfg.Load() + if cfg.remoteConfig.RegistrationRefresh != tickerDuration { + tickerDuration = cfg.remoteConfig.RegistrationRefresh + ticker.Reset(tickerDuration) + } + + s.mu.RLock() + s.lggr.Infow("register trigger for remote capability", "donId", cfg.capDonInfo.ID, "nMembers", len(cfg.capDonInfo.Members), "nWorkflows", len(s.registeredWorkflows)) + var totalRegistrations, totalP2PSends, totalSendErrors int + for _, regMap := range s.registeredWorkflows { + totalRegistrations += len(regMap) + } + s.lggr.Infow("registrationLoop tick: sending registrations", + "donId", cfg.capDonInfo.ID, + "nCapDonMembers", len(cfg.capDonInfo.Members), + "nWorkflows", len(s.registeredWorkflows), + "nRegistrations", totalRegistrations, + "expectedP2PSends", totalRegistrations*len(cfg.capDonInfo.Members)) + for _, regMap := range s.registeredWorkflows { + for _, registration := range regMap { + for _, peerID := range cfg.capDonInfo.Members { + m := &types.MessageBody{ + CapabilityId: cfg.capInfo.ID, + CapabilityDonId: cfg.capDonInfo.ID, + CallerDonId: cfg.localDonID, + Method: types.MethodRegisterTrigger, + Payload: registration.rawRequest, // triggerID is in the raw request + CapabilityMethod: s.capMethodName, + } + err := s.dispatcher.Send(peerID, m) + if err != nil { + totalSendErrors++ + s.lggr.Errorw("failed to send message", "donId", cfg.capDonInfo.ID, "peerId", peerID, "err", err) + } else { + totalP2PSends++ + } + } + } + } + s.mu.RUnlock() + s.lggr.Infow("registrationLoop tick: completed", + "donId", cfg.capDonInfo.ID, + "p2pSendsSent", totalP2PSends, + "p2pSendErrors", totalSendErrors) + } + } +} + +func (s *triggerSubscriber) UnregisterTrigger(ctx context.Context, request commoncap.TriggerRegistrationRequest) error { + s.mu.Lock() + defer s.mu.Unlock() + + triggerMap, ok := s.registeredWorkflows[request.Metadata.WorkflowID] + if !ok { + return nil + } + state := triggerMap[request.TriggerID] + if state != nil && state.callback != nil { + close(state.callback) + } + delete(triggerMap, request.TriggerID) + if len(triggerMap) == 0 { + delete(s.registeredWorkflows, request.Metadata.WorkflowID) + } + // Registrations will quickly expire on all remote nodes. + // Alternatively, we could send UnregisterTrigger messages right away. + return nil +} + +func (s *triggerSubscriber) Receive(_ context.Context, msg *types.MessageBody) { + sender, err := ToPeerID(msg.Sender) + if err != nil { + s.lggr.Errorw("failed to convert message sender to PeerID", "err", err) + return + } + cfg := s.cfg.Load() + if cfg == nil { + s.lggr.Errorw("config not set - call SetConfig() first") + return + } + if _, found := cfg.capDonMembers[sender]; !found { + s.lggr.Errorw("received message from unexpected node", "sender", sender) + return + } + + switch msg.Method { + case types.MethodTriggerEvent: + meta := msg.GetTriggerEventMetadata() + if meta == nil { + s.lggr.Errorw("received message with invalid trigger metadata", "sender", sender) + return + } + if len(meta.WorkflowIds) > maxBatchedWorkflowIDs { + s.lggr.Errorw("received message with too many workflow IDs - truncating", "nWorkflows", len(meta.WorkflowIds), "sender", sender) + meta.WorkflowIds = meta.WorkflowIds[:maxBatchedWorkflowIDs] + } + for idx, workflowID := range meta.WorkflowIds { + var triggerID string + if idx < len(meta.TriggerIds) { + triggerID = meta.TriggerIds[idx] + } + s.mu.RLock() + triggerMap, found := s.registeredWorkflows[workflowID] + var registration *subRegState + if found { + if triggerID != "" { + // received a message from updated publisher, which provided a triggerID + registration = triggerMap[triggerID] + } else { + // legacy flow, expect there to be only a single trigger of each type per workflow + for tid, reg := range triggerMap { + registration = reg + triggerID = tid // canonical registration id for caches and AckEvent replay + break + } + if len(triggerMap) > 1 { + s.lggr.Errorw("received message without triggerID but workflow has multiple trigger - picking a random one", "workflowID", SanitizeLogString(workflowID), "sender", sender) + } + } + } + s.mu.RUnlock() + if registration == nil { + s.lggr.Errorw("received message for unregistered workflow/trigger", "workflowID", SanitizeLogString(workflowID), "triggerID", triggerID, "sender", sender) + continue + } + + key := triggerEventKey{ + triggerEventID: meta.TriggerEventId, + workflowID: workflowID, + triggerID: triggerID, + } + rk := ackReplayKey{triggerID: triggerID, triggerEventID: meta.TriggerEventId} + s.mu.Lock() + if _, ok := s.ackReplayCache[rk]; ok { + // Event has already been ACKd by engine, so we don't need to re-deliver + s.mu.Unlock() + ctx, cancel := s.stopCh.NewCtx() + err := s.AckEvent(ctx, triggerID, meta.TriggerEventId, s.capMethodName) + cancel() + if err != nil { + s.lggr.Errorw("replay AckEvent failed", "triggerID", triggerID, "triggerEventID", meta.TriggerEventId, "err", err) + } else { + s.lggr.Infow("replayed ACK fan-out for duplicate trigger event after prior engine ACK", + "triggerID", triggerID, "triggerEventID", meta.TriggerEventId, "sender", sender) + } + continue + } + + nowMs := time.Now().UnixMilli() + creationTs := s.messageCache.Insert(key, sender, nowMs, msg.Payload) + ready, payloads := s.messageCache.Ready(key, cfg.remoteConfig.MinResponsesToAggregate, nowMs-cfg.remoteConfig.MessageExpiry.Milliseconds(), true) + s.mu.Unlock() + s.lggr.Debugw("trigger event received", "triggerEventId", meta.TriggerEventId, "workflowId", workflowID, "triggerID", triggerID, "sender", sender, "ready", ready, "nowTs", nowMs, "creationTs", creationTs, "minResponsesToAggregate", cfg.remoteConfig.MinResponsesToAggregate) + if ready { + aggregatedResponse, err := cfg.aggregator.Aggregate(meta.TriggerEventId, payloads) + if err != nil { + s.lggr.Errorw("failed to aggregate responses", "triggerEventID", meta.TriggerEventId, "workflowId", workflowID, "triggerID", triggerID, "err", err) + continue + } + s.lggr.Infow("remote trigger event aggregated", "triggerEventID", meta.TriggerEventId, "workflowId", workflowID, "triggerID", triggerID) + registration.callback <- aggregatedResponse + } + } + case types.MethodTriggerRegistrationCheck: + meta := msg.GetTriggerEventMetadata() + if meta == nil { + s.lggr.Errorw("received registration check with nil metadata", "sender", sender) + return + } + + if len(meta.WorkflowIds) != len(meta.TriggerIds) { + s.lggr.Errorw("received registration check with mismatched workflow and trigger IDs", + "sender", sender, + "workflowIdsLen", len(meta.WorkflowIds), + "triggerIdsLen", len(meta.TriggerIds)) + return + } + + s.lggr.Infow("received registration check", "sender", sender) + for i, workflowID := range meta.WorkflowIds { + triggerID := meta.TriggerIds[i] + + s.mu.RLock() + triggerMap, ok := s.registeredWorkflows[workflowID] + reg := ok && triggerMap[triggerID] != nil + s.mu.RUnlock() + + if !reg { + s.lggr.Infow("sending unregister in response to registration check", "workflowID", workflowID, "triggerID", triggerID) + // Registration was removed locally — tell the publisher to clean up. + s.sendUnregister(workflowID, triggerID) + } + // For existing registrations we intentionally do NOT resend. + // The periodic registrationLoop already refreshes registrations, + // and the publisher ignores duplicate MethodRegisterTrigger for + // registrations it already has. + } + default: + s.lggr.Errorw("received trigger event with unknown method", "method", SanitizeLogString(msg.Method), "sender", sender, "err", SanitizeLogString(msg.ErrorMsg)) + } +} + +func (s *triggerSubscriber) sendUnregister(workflowID, triggerID string) { + if s.dispatcher == nil { + return + } + + cfg := s.cfg.Load() + + for _, peerID := range cfg.capDonInfo.Members { + m := &types.MessageBody{ + CapabilityId: cfg.capInfo.ID, + CapabilityDonId: cfg.capDonInfo.ID, + CallerDonId: cfg.localDonID, + Method: types.MethodUnregisterTrigger, + CapabilityMethod: s.capMethodName, + Metadata: &types.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &types.TriggerEventMetadata{ + WorkflowIds: []string{workflowID}, + TriggerIds: []string{triggerID}, + }, + }, + } + err := s.dispatcher.Send(peerID, m) + if err != nil { + s.lggr.Errorw("failed to send message", "donId", cfg.capDonInfo.ID, "peerId", peerID, "err", err) + } + } +} + +func (s *triggerSubscriber) eventCleanupLoop() { + defer s.wg.Done() + cfg := s.cfg.Load() + cleanupInterval := cfg.remoteConfig.MessageExpiry + ticker := time.NewTicker(cleanupInterval) + defer ticker.Stop() + for { + select { + case <-s.stopCh: + return + case <-ticker.C: + freshCfg := s.cfg.Load() + remoteConfig := freshCfg.remoteConfig + // Update cleanup interval if config has changed + if remoteConfig.MessageExpiry != cleanupInterval { + cleanupInterval = remoteConfig.MessageExpiry + ticker.Reset(cleanupInterval) + } + s.mu.Lock() + s.messageCache.DeleteOlderThan(time.Now().UnixMilli() - remoteConfig.MessageExpiry.Milliseconds()) + cutoff := time.Now().UnixMilli() - remoteConfig.MessageExpiry.Milliseconds() + for k, ts := range s.ackReplayCache { + if ts < cutoff { + delete(s.ackReplayCache, k) + } + } + s.mu.Unlock() + } + } +} + +func (s *triggerSubscriber) Close() error { + close(s.stopCh) + s.wg.Wait() + s.lggr.Info("TriggerSubscriber closed") + return nil +} + +func (s *triggerSubscriber) Ready() error { + return nil +} + +func (s *triggerSubscriber) HealthReport() map[string]error { + return nil +} + +func (s *triggerSubscriber) Name() string { + return s.lggr.Name() +} + +// SetConfig sets the remote trigger configuration, capability info, and DON information dynamically +func (s *triggerSubscriber) SetConfig(config *commoncap.RemoteTriggerConfig, capInfo commoncap.CapabilityInfo, localDONID uint32, remoteDON commoncap.DON, aggregator types.Aggregator) error { + if config == nil { + s.lggr.Info("SetConfig called with nil config, using defaults") + config = &commoncap.RemoteTriggerConfig{} + } + config.ApplyDefaults() + if capInfo.ID == "" || capInfo.ID != s.capabilityID { + return fmt.Errorf("capability info provided does not match the subscriber's capabilityID: %s != %s", capInfo.ID, s.capabilityID) + } + if localDONID == 0 { + return errors.New("localDONID=0 provided") + } + if remoteDON.ID == 0 || len(remoteDON.Members) == 0 { + return errors.New("empty remoteDON provided") + } + if aggregator == nil { + return errors.New("aggregator not set - call SetAggregator() before SetConfig()") + } + // Rebuild the capDonMembers map + capDonMembers := make(map[rage.PeerID]struct{}) + for _, member := range remoteDON.Members { + capDonMembers[member] = struct{}{} + } + + // always replace the whole dynamicConfig object to avoid inconsistent state + s.cfg.Store(&dynamicConfig{ + remoteConfig: config, + capInfo: capInfo, + capDonInfo: remoteDON, + capDonMembers: capDonMembers, + localDonID: localDONID, + aggregator: aggregator, + }) + return nil +} diff --git a/libs/x/don2don/trigger_subscriber_test.go b/libs/x/don2don/trigger_subscriber_test.go new file mode 100644 index 000000000..90285f1cb --- /dev/null +++ b/libs/x/don2don/trigger_subscriber_test.go @@ -0,0 +1,767 @@ +package don2don_test + +import ( + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + "github.com/smartcontractkit/capabilities/libs/x/don2don/aggregation" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + remoteMocks "github.com/smartcontractkit/capabilities/libs/x/don2don/types/mocks" + "github.com/smartcontractkit/capabilities/libs/x/internal/synctest" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +const ( + peerID1 = "12D3KooWF3dVeJ6YoT5HFnYhmwQWWMoEwVFzJQ5kKCMX3ZityxMC" + peerID2 = "12D3KooWQsmok6aD8PZqt3RnJhQRrNzKHLficq7zYFRp7kZ1hHP8" + peerID3 = "12D3KooWBCF1XT5Wi8FzfgNCqRL76Swv8TRU3TiD4QiJm8NMNX7N" + workflowID1 = "15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0" +) + +var ( + triggerEvent1 = map[string]any{"event": "triggerEvent1"} +) + +func TestTriggerSubscriber_RegisterAndReceive(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, 1, 1) + dispatcher := remoteMocks.NewDispatcher(t) + dispatcher.On("Send", mock.Anything, mock.Anything).Return(nil).Maybe() + + // register trigger + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + } + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(config.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(config, capInfo, workflowDon.ID, capDon, agg)) + require.NoError(t, subscriber.Start(t.Context())) + + req := commoncap.TriggerRegistrationRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + triggerEventCallbackCh, err := subscriber.RegisterTrigger(t.Context(), req) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req)) + // calling UnregisterTrigger repeatedly is safe + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req)) + require.NoError(t, subscriber.Close()) + }) + + // receive trigger event + triggerEventValue, err := values.NewMap(triggerEvent1) + require.NoError(t, err) + triggerEvent := buildTriggerEvent(t, capDon.Members[0][:]) + subscriber.Receive(t.Context(), triggerEvent) + response := <-triggerEventCallbackCh + require.Equal(t, response.Event.Outputs, triggerEventValue) +} + +func TestTriggerSubscriber_CorrectEventExpiryCheck(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + const ( + messageExpiry = 10 * time.Second + staleNodeDelay = 2 * time.Second + freshNodesExtraWait = 13 * time.Second // staleNodeDelay + freshNodesExtraWait = 15s total + ) + + cfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: time.Hour, + MinResponsesToAggregate: 2, + MessageExpiry: messageExpiry, + } + regReq := commoncap.TriggerRegistrationRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + subscriber, capDon, callbackCh := newTriggerSubscriberForTest(t, 3, cfg, regReq) + + expectedOutputs, err := values.NewMap(triggerEvent1) + require.NoError(t, err) + triggerEvent := buildTriggerEvent(t, capDon.Members[0][:]) + + // Phase 1: peer0 sends early; message cached but not ready (need 2 peers). + time.Sleep(staleNodeDelay) + subscriber.Receive(t.Context(), triggerEvent) + + // Phase 2: advance past the T=10s cleanup tick. Cleanup uses cutoff 0, so the + // event created at T=2s is retained. At T=15s peer0 is outside the 10s window. + time.Sleep(freshNodesExtraWait) + triggerEvent.Sender = capDon.Members[1][:] + subscriber.Receive(t.Context(), triggerEvent) + require.Empty(t, callbackCh, "stale peer0 + one fresh peer must not aggregate") + + triggerEvent.Sender = capDon.Members[2][:] + subscriber.Receive(t.Context(), triggerEvent) + + response := <-callbackCh + require.Equal(t, expectedOutputs, response.Event.Outputs) + }) +} + +func TestTriggerSubscriber_SetConfig_Basic(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, 3, 1) + agg := aggregation.NewDefaultModeAggregator(1) + + t.Run("returns error when capability info ID doesn't match subscriber's ID", func(t *testing.T) { + t.Parallel() + + dispatcher := remoteMocks.NewDispatcher(t) + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + config := &commoncap.RemoteTriggerConfig{} + mismatchedCapInfo := commoncap.CapabilityInfo{ID: "different_id", CapabilityType: commoncap.CapabilityTypeTrigger} + err := subscriber.SetConfig(config, mismatchedCapInfo, workflowDon.ID, capDon, agg) + require.Error(t, err) + require.Contains(t, err.Error(), "capability info provided does not match") + require.Contains(t, err.Error(), "different_id") + require.Contains(t, err.Error(), capInfo.ID) + }) + + t.Run("returns error when aggregator is nil", func(t *testing.T) { + t.Parallel() + + dispatcher := remoteMocks.NewDispatcher(t) + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + config := &commoncap.RemoteTriggerConfig{} + err := subscriber.SetConfig(config, capInfo, workflowDon.ID, capDon, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "aggregator not set") + }) + + t.Run("updates existing config", func(t *testing.T) { + t.Parallel() + + dispatcher := remoteMocks.NewDispatcher(t) + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + // Set initial config + initialConfig := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + } + err := subscriber.SetConfig(initialConfig, capInfo, workflowDon.ID, capDon, agg) + require.NoError(t, err) + + // Update with new config + newConfig := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 500 * time.Millisecond, + MinResponsesToAggregate: 3, + MessageExpiry: 500 * time.Second, + } + err = subscriber.SetConfig(newConfig, capInfo, workflowDon.ID, capDon, agg) + require.NoError(t, err) + + // Verify updated config works + require.NoError(t, subscriber.Start(t.Context())) + require.NoError(t, subscriber.Close()) + }) + t.Run("handles nil initial config", func(t *testing.T) { + t.Parallel() + + dispatcher := remoteMocks.NewDispatcher(t) + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + // Set initial config as nil + err := subscriber.SetConfig(nil, capInfo, workflowDon.ID, capDon, agg) + require.NoError(t, err) + + // Verify config works + require.NoError(t, subscriber.Start(t.Context())) + require.NoError(t, subscriber.Close()) + }) +} + +func TestTriggerSubscriber_MultipleTriggersSameWorkflow(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, 1, 1) + dispatcher := remoteMocks.NewDispatcher(t) + dispatcher.On("Send", mock.Anything, mock.Anything).Return(nil).Maybe() + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + } + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(config.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(config, capInfo, workflowDon.ID, capDon, agg)) + require.NoError(t, subscriber.Start(t.Context())) + + // Register two triggers for the same workflow with different triggerIDs + req1 := commoncap.TriggerRegistrationRequest{ + TriggerID: "trigger1", + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + req2 := commoncap.TriggerRegistrationRequest{ + TriggerID: "trigger2", + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + + callbackCh1, err := subscriber.RegisterTrigger(t.Context(), req1) + require.NoError(t, err) + callbackCh2, err := subscriber.RegisterTrigger(t.Context(), req2) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req1)) + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req2)) + require.NoError(t, subscriber.Close()) + }) + + // Send message for trigger1 - should only go to callbackCh1 + triggerEvent1Msg := buildTriggerEventWithTriggerID(t, capDon.Members[0][:], workflowID1, "trigger1", "event1") + subscriber.Receive(t.Context(), triggerEvent1Msg) + + resp := <-callbackCh1 + require.NotNil(t, resp.Event.Outputs) + + select { + case <-callbackCh2: + t.Fatal("did not expect message on callbackCh2") + default: + // expected - no message on callbackCh2 + } + + // Send message for trigger2 - should only go to callbackCh2 + triggerEvent2Msg := buildTriggerEventWithTriggerID(t, capDon.Members[0][:], workflowID1, "trigger2", "event2") + subscriber.Receive(t.Context(), triggerEvent2Msg) + + resp = <-callbackCh2 + require.NotNil(t, resp.Event.Outputs) + + select { + case <-callbackCh1: + t.Fatal("did not expect another message on callbackCh1") + default: + // expected - no message on callbackCh1 + } +} + +func TestTriggerSubscriber_LegacyMessageWithoutTriggerID(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, 1, 1) + dispatcher := remoteMocks.NewDispatcher(t) + dispatcher.On("Send", mock.Anything, mock.Anything).Return(nil).Maybe() + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + } + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(config.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(config, capInfo, workflowDon.ID, capDon, agg)) + require.NoError(t, subscriber.Start(t.Context())) + + // Register single trigger (legacy style, with triggerID but receiving messages without it) + req := commoncap.TriggerRegistrationRequest{ + TriggerID: "trigger1", + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + + callbackCh, err := subscriber.RegisterTrigger(t.Context(), req) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req)) + require.NoError(t, subscriber.Close()) + }) + + // Send legacy message without triggerID - should still route to the single registered trigger + legacyMsg := buildTriggerEvent(t, capDon.Members[0][:]) + subscriber.Receive(t.Context(), legacyMsg) + + resp := <-callbackCh + require.NotNil(t, resp.Event.Outputs) +} + +func TestTriggerSubscriber_AckReplayOnDuplicateReceive(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, 3, 1) + dispatcher := remoteMocks.NewDispatcher(t) + // Count ACK fan-out sends through a synchronized hook rather than reading the + // mock's internal Calls slice directly: the background registrationLoop keeps + // calling Send concurrently, so touching dispatcher.Calls unsynchronized races. + // Filtering by method isolates ACKs from the loop's MethodRegisterTrigger sends. + var ackSendsTotal atomic.Int64 + dispatcher.On("Send", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + if m, ok := args.Get(1).(*don2dontypes.MessageBody); ok && m.Method == don2dontypes.MethodTriggerEventAck { + ackSendsTotal.Add(1) + } + }).Return(nil).Maybe() + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + } + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(config.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(config, capInfo, workflowDon.ID, capDon, agg)) + require.NoError(t, subscriber.Start(t.Context())) + + triggerRegID := fmt.Sprintf("trigger_reg_%s_%d", workflowID1, 0) + req := commoncap.TriggerRegistrationRequest{ + TriggerID: triggerRegID, + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + callbackCh, err := subscriber.RegisterTrigger(t.Context(), req) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req)) + require.NoError(t, subscriber.Close()) + }) + + eventID := "event-ack-replay-1" + msg := buildTriggerEventWithTriggerID(t, capDon.Members[0][:], workflowID1, triggerRegID, eventID) + subscriber.Receive(t.Context(), msg) + <-callbackCh + + require.NoError(t, subscriber.AckEvent(t.Context(), triggerRegID, eventID, "method")) + + // Duplicate receive replays the ACK fan-out synchronously within Receive. + before := ackSendsTotal.Load() + subscriber.Receive(t.Context(), msg) + + require.Equal(t, int64(len(capDon.Members)), ackSendsTotal.Load()-before, "duplicate receive should fan out ACK to all capability DON members") + + select { + case r := <-callbackCh: + t.Fatalf("expected no second aggregated delivery to engine, got %+v", r) + default: + } +} + +func TestTriggerSubscriber_UnregisterOneTriggerKeepsOther(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, 1, 1) + dispatcher := remoteMocks.NewDispatcher(t) + dispatcher.On("Send", mock.Anything, mock.Anything).Return(nil).Maybe() + + config := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: 100 * time.Millisecond, + RegistrationExpiry: 100 * time.Second, + MinResponsesToAggregate: 1, + MessageExpiry: 100 * time.Second, + } + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(config.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(config, capInfo, workflowDon.ID, capDon, agg)) + require.NoError(t, subscriber.Start(t.Context())) + + // Register two triggers + req1 := commoncap.TriggerRegistrationRequest{ + TriggerID: "trigger1", + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + req2 := commoncap.TriggerRegistrationRequest{ + TriggerID: "trigger2", + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + + _, err := subscriber.RegisterTrigger(t.Context(), req1) + require.NoError(t, err) + callbackCh2, err := subscriber.RegisterTrigger(t.Context(), req2) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req2)) + require.NoError(t, subscriber.Close()) + }) + + // Unregister trigger1 + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), req1)) + + // trigger2 should still work + triggerEvent2Msg := buildTriggerEventWithTriggerID(t, capDon.Members[0][:], workflowID1, "trigger2", "event2") + subscriber.Receive(t.Context(), triggerEvent2Msg) + + resp := <-callbackCh2 + require.NotNil(t, resp.Event.Outputs) +} + +type subscriberSvc interface { + don2don.TriggerSubscriber + services.Service +} + +func TestTriggerSubscriber_RegistrationCheck(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, 1, 1) + + newSubscriber := func(t *testing.T) (subscriberSvc, *remoteMocks.Dispatcher) { + dispatcher := remoteMocks.NewDispatcher(t) + dispatcher.On("Send", mock.Anything, mock.Anything).Return(nil).Maybe() + + cfg := &commoncap.RemoteTriggerConfig{ + RegistrationRefresh: time.Hour, + MinResponsesToAggregate: 1, + MessageExpiry: time.Minute, + } + + sub := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(1) + + require.NoError(t, sub.SetConfig(cfg, capInfo, workflowDon.ID, capDon, agg)) + require.NoError(t, sub.Start(t.Context())) + + t.Cleanup(func() { + require.NoError(t, sub.Close()) + }) + + return sub, dispatcher + } + + buildCheckMsg := func(workflowID, triggerID string) *don2dontypes.MessageBody { + return &don2dontypes.MessageBody{ + Sender: capDon.Members[0][:], + Method: don2dontypes.MethodTriggerRegistrationCheck, + CallerDonId: workflowDon.ID, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID}, + TriggerIds: []string{triggerID}, + }, + }, + } + } + + t.Run("does not resend registration when trigger exists", func(t *testing.T) { + t.Parallel() + + sub, dispatcher := newSubscriber(t) + + _, err := sub.RegisterTrigger(t.Context(), commoncap.TriggerRegistrationRequest{ + TriggerID: "triggerA", + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + }) + require.NoError(t, err) + + dispatcher.Calls = nil + sub.Receive(t.Context(), buildCheckMsg(workflowID1, "triggerA")) + + // With the batching optimization, the subscriber no longer resends + // MethodRegisterTrigger in response to a check for an existing + // registration — the periodic registrationLoop handles that, and + // the publisher ignores duplicate re-registrations anyway. Only + // unregisters are sent for missing registrations. + for _, call := range dispatcher.Calls { + if call.Method != "Send" { + continue + } + msg := call.Arguments.Get(1).(*don2dontypes.MessageBody) + require.NotEqual(t, don2dontypes.MethodRegisterTrigger, msg.Method, + "should not resend registration for existing trigger") + } + }) + + t.Run("sends unregister when trigger missing with correct metadata", func(t *testing.T) { + t.Parallel() + + sub, dispatcher := newSubscriber(t) + + dispatcher.Calls = nil + sub.Receive(t.Context(), buildCheckMsg(workflowID1, "triggerA")) + + var found bool + for _, call := range dispatcher.Calls { + if call.Method != "Send" { + continue + } + msg := call.Arguments.Get(1).(*don2dontypes.MessageBody) + if msg.Method == don2dontypes.MethodUnregisterTrigger { + require.Equal(t, capInfo.ID, msg.CapabilityId) + require.Equal(t, capDon.ID, msg.CapabilityDonId) + require.Equal(t, workflowDon.ID, msg.CallerDonId) + meta := msg.GetTriggerEventMetadata() + require.NotNil(t, meta) + require.Equal(t, []string{workflowID1}, meta.WorkflowIds) + require.Equal(t, []string{"triggerA"}, meta.TriggerIds) + found = true + break + } + } + require.True(t, found, "expected a MethodUnregisterTrigger Send call") + }) + + t.Run("sends unregister after trigger is unregistered locally", func(t *testing.T) { + t.Parallel() + + sub, dispatcher := newSubscriber(t) + + req := commoncap.TriggerRegistrationRequest{ + TriggerID: "triggerA", + Metadata: commoncap.RequestMetadata{ + WorkflowID: workflowID1, + }, + } + _, err := sub.RegisterTrigger(t.Context(), req) + require.NoError(t, err) + + // While registered, a check should NOT send anything (no resend). + dispatcher.Calls = nil + sub.Receive(t.Context(), buildCheckMsg(workflowID1, "triggerA")) + for _, call := range dispatcher.Calls { + if call.Method != "Send" { + continue + } + msg := call.Arguments.Get(1).(*don2dontypes.MessageBody) + require.NotEqual(t, don2dontypes.MethodRegisterTrigger, msg.Method, + "should not resend registration for existing trigger") + } + + // Unregister locally + require.NoError(t, sub.UnregisterTrigger(t.Context(), req)) + + // Now the same check should result in unregister + dispatcher.Calls = nil + sub.Receive(t.Context(), buildCheckMsg(workflowID1, "triggerA")) + dispatcher.AssertCalled(t, "Send", mock.Anything, mock.MatchedBy(func(m *don2dontypes.MessageBody) bool { + return m.Method == don2dontypes.MethodUnregisterTrigger + })) + }) + + t.Run("ignores check with mismatched WorkflowIds and TriggerIds lengths", func(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + workflowIDs []string + triggerIDs []string + }{ + { + name: "more WorkflowIds than TriggerIds", + workflowIDs: []string{workflowID1}, + triggerIDs: nil, + }, + { + name: "more TriggerIds than WorkflowIds", + workflowIDs: nil, + triggerIDs: []string{"triggerA"}, + }, + { + name: "multiple WorkflowIds with shorter TriggerIds", + workflowIDs: []string{workflowID1, "workflow2", "workflow3"}, + triggerIDs: []string{"triggerA"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + sub, dispatcher := newSubscriber(t) + + malformedMsg := &don2dontypes.MessageBody{ + Sender: capDon.Members[0][:], + Method: don2dontypes.MethodTriggerRegistrationCheck, + CallerDonId: workflowDon.ID, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: tc.workflowIDs, + TriggerIds: tc.triggerIDs, + }, + }, + } + + dispatcher.Calls = nil + // Must not panic. + sub.Receive(t.Context(), malformedMsg) + + for _, call := range dispatcher.Calls { + if call.Method == "Send" { + t.Errorf("expected no dispatcher.Send for malformed check message, got method=%v", + call.Arguments.Get(1).(*don2dontypes.MessageBody).Method) + } + } + }) + } + }) + + t.Run("ignores check from unknown sender", func(t *testing.T) { + t.Parallel() + + sub, dispatcher := newSubscriber(t) + + unknownPeer := rage.PeerID{0xaa} + checkMsg := &don2dontypes.MessageBody{ + Sender: unknownPeer[:], + Method: don2dontypes.MethodTriggerRegistrationCheck, + CallerDonId: workflowDon.ID, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID1}, + TriggerIds: []string{"triggerA"}, + }, + }, + } + + dispatcher.Calls = nil + sub.Receive(t.Context(), checkMsg) + + // No registration or unregistration calls should have been made + for _, call := range dispatcher.Calls { + if call.Method == "Send" { + msg := call.Arguments.Get(1).(*don2dontypes.MessageBody) + require.NotEqual(t, don2dontypes.MethodRegisterTrigger, msg.Method, "should not re-register from unknown sender") + require.NotEqual(t, don2dontypes.MethodUnregisterTrigger, msg.Method, "should not unregister from unknown sender") + } + } + }) +} + +func newTriggerSubscriberForTest( + t *testing.T, + capDonMembers int, + cfg *commoncap.RemoteTriggerConfig, + regReq commoncap.TriggerRegistrationRequest, +) (don2don.TriggerSubscriber, commoncap.DON, <-chan commoncap.TriggerResponse) { + t.Helper() + + lggr := logger.Test(t) + capInfo, capDon, workflowDon := buildTwoTestDONs(t, capDonMembers, 1) + dispatcher := remoteMocks.NewDispatcher(t) + dispatcher.On("Send", mock.Anything, mock.Anything).Return(nil).Maybe() + + subscriber := don2don.NewTriggerSubscriber(capInfo.ID, "method", dispatcher, lggr) + agg := aggregation.NewDefaultModeAggregator(cfg.MinResponsesToAggregate) + require.NoError(t, subscriber.SetConfig(cfg, capInfo, workflowDon.ID, capDon, agg)) + require.NoError(t, subscriber.Start(t.Context())) + + callbackCh, err := subscriber.RegisterTrigger(t.Context(), regReq) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, subscriber.UnregisterTrigger(t.Context(), regReq)) + require.NoError(t, subscriber.Close()) + }) + + return subscriber, capDon, callbackCh +} + +func buildTwoTestDONs(t *testing.T, capDonSize int, workflowDonSize int) (commoncap.CapabilityInfo, commoncap.DON, commoncap.DON) { + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1", + CapabilityType: commoncap.CapabilityTypeTrigger, + Description: "Remote Trigger", + } + + capDon := commoncap.DON{ + ID: 1, + Members: []rage.PeerID{}, + F: 0, + } + for range capDonSize { + pid := rage.MustNewPeerID() + peer := rage.PeerID{} + require.NoError(t, peer.UnmarshalText([]byte(pid))) + capDon.Members = append(capDon.Members, peer) + } + + workflowDon := commoncap.DON{ + ID: 2, + Members: []rage.PeerID{}, + F: 0, + } + for range workflowDonSize { + pid := rage.MustNewPeerID() + peer := rage.PeerID{} + require.NoError(t, peer.UnmarshalText([]byte(pid))) + workflowDon.Members = append(workflowDon.Members, peer) + } + return capInfo, capDon, workflowDon +} + +func buildTriggerEvent(t *testing.T, sender []byte) *don2dontypes.MessageBody { + triggerEventValue, err := values.NewMap(triggerEvent1) + require.NoError(t, err) + capResponse := commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ + Outputs: triggerEventValue, + }, + Err: nil, + } + marshaled, err := pb.MarshalTriggerResponse(capResponse) + require.NoError(t, err) + + return &don2dontypes.MessageBody{ + Sender: sender, + Method: don2dontypes.MethodTriggerEvent, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + WorkflowIds: []string{workflowID1}, + }, + }, + Payload: marshaled, + } +} + +func buildTriggerEventWithTriggerID(t *testing.T, sender []byte, workflowID, triggerID, eventID string) *don2dontypes.MessageBody { + triggerEventValue, err := values.NewMap(triggerEvent1) + require.NoError(t, err) + capResponse := commoncap.TriggerResponse{ + Event: commoncap.TriggerEvent{ + ID: eventID, + Outputs: triggerEventValue, + }, + Err: nil, + } + marshaled, err := pb.MarshalTriggerResponse(capResponse) + require.NoError(t, err) + + return &don2dontypes.MessageBody{ + Sender: sender, + Method: don2dontypes.MethodTriggerEvent, + Metadata: &don2dontypes.MessageBody_TriggerEventMetadata{ + TriggerEventMetadata: &don2dontypes.TriggerEventMetadata{ + TriggerEventId: eventID, + WorkflowIds: []string{workflowID}, + TriggerIds: []string{triggerID}, + }, + }, + Payload: marshaled, + } +} diff --git a/libs/x/don2don/types/messages.pb.go b/libs/x/don2don/types/messages.pb.go new file mode 100644 index 000000000..573078cb4 --- /dev/null +++ b/libs/x/don2don/types/messages.pb.go @@ -0,0 +1,528 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: x/don2don/types/messages.proto + +package types + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Error int32 + +const ( + Error_OK Error = 0 + Error_VALIDATION_FAILED Error = 1 + Error_CAPABILITY_NOT_FOUND Error = 2 + Error_INVALID_REQUEST Error = 3 + Error_TIMEOUT Error = 4 + Error_INTERNAL_ERROR Error = 5 +) + +// Enum value maps for Error. +var ( + Error_name = map[int32]string{ + 0: "OK", + 1: "VALIDATION_FAILED", + 2: "CAPABILITY_NOT_FOUND", + 3: "INVALID_REQUEST", + 4: "TIMEOUT", + 5: "INTERNAL_ERROR", + } + Error_value = map[string]int32{ + "OK": 0, + "VALIDATION_FAILED": 1, + "CAPABILITY_NOT_FOUND": 2, + "INVALID_REQUEST": 3, + "TIMEOUT": 4, + "INTERNAL_ERROR": 5, + } +) + +func (x Error) Enum() *Error { + p := new(Error) + *p = x + return p +} + +func (x Error) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Error) Descriptor() protoreflect.EnumDescriptor { + return file_x_don2don_types_messages_proto_enumTypes[0].Descriptor() +} + +func (Error) Type() protoreflect.EnumType { + return &file_x_don2don_types_messages_proto_enumTypes[0] +} + +func (x Error) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Error.Descriptor instead. +func (Error) EnumDescriptor() ([]byte, []int) { + return file_x_don2don_types_messages_proto_rawDescGZIP(), []int{0} +} + +type Message struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` // proto-encoded MessageBody to sign + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Message) Reset() { + *x = Message{} + mi := &file_x_don2don_types_messages_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_x_don2don_types_messages_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_x_don2don_types_messages_proto_rawDescGZIP(), []int{0} +} + +func (x *Message) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +func (x *Message) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +type MessageBody struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Sender []byte `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` + Receiver []byte `protobuf:"bytes,3,opt,name=receiver,proto3" json:"receiver,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + MessageId []byte `protobuf:"bytes,5,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` // scoped to sender + CapabilityId string `protobuf:"bytes,6,opt,name=capability_id,json=capabilityId,proto3" json:"capability_id,omitempty"` + Method string `protobuf:"bytes,9,opt,name=method,proto3" json:"method,omitempty"` // Capability API method ("RegisterTrigger", "Execute", etc) + Error Error `protobuf:"varint,10,opt,name=error,proto3,enum=remote.Error" json:"error,omitempty"` + ErrorMsg string `protobuf:"bytes,11,opt,name=errorMsg,proto3" json:"errorMsg,omitempty"` + // payload contains a CapabilityRequest or CapabilityResponse + Payload []byte `protobuf:"bytes,12,opt,name=payload,proto3" json:"payload,omitempty"` + // Types that are valid to be assigned to Metadata: + // + // *MessageBody_TriggerRegistrationMetadata + // *MessageBody_TriggerEventMetadata + Metadata isMessageBody_Metadata `protobuf_oneof:"metadata"` + CapabilityDonId uint32 `protobuf:"varint,15,opt,name=capability_don_id,json=capabilityDonId,proto3" json:"capability_don_id,omitempty"` + CallerDonId uint32 `protobuf:"varint,16,opt,name=caller_don_id,json=callerDonId,proto3" json:"caller_don_id,omitempty"` + CapabilityMethod string `protobuf:"bytes,17,opt,name=capability_method,json=capabilityMethod,proto3" json:"capability_method,omitempty"` // method name defined by the capability (empty for legacy "v1" calls) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageBody) Reset() { + *x = MessageBody{} + mi := &file_x_don2don_types_messages_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageBody) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageBody) ProtoMessage() {} + +func (x *MessageBody) ProtoReflect() protoreflect.Message { + mi := &file_x_don2don_types_messages_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageBody.ProtoReflect.Descriptor instead. +func (*MessageBody) Descriptor() ([]byte, []int) { + return file_x_don2don_types_messages_proto_rawDescGZIP(), []int{1} +} + +func (x *MessageBody) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *MessageBody) GetSender() []byte { + if x != nil { + return x.Sender + } + return nil +} + +func (x *MessageBody) GetReceiver() []byte { + if x != nil { + return x.Receiver + } + return nil +} + +func (x *MessageBody) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *MessageBody) GetMessageId() []byte { + if x != nil { + return x.MessageId + } + return nil +} + +func (x *MessageBody) GetCapabilityId() string { + if x != nil { + return x.CapabilityId + } + return "" +} + +func (x *MessageBody) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *MessageBody) GetError() Error { + if x != nil { + return x.Error + } + return Error_OK +} + +func (x *MessageBody) GetErrorMsg() string { + if x != nil { + return x.ErrorMsg + } + return "" +} + +func (x *MessageBody) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *MessageBody) GetMetadata() isMessageBody_Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *MessageBody) GetTriggerRegistrationMetadata() *TriggerRegistrationMetadata { + if x != nil { + if x, ok := x.Metadata.(*MessageBody_TriggerRegistrationMetadata); ok { + return x.TriggerRegistrationMetadata + } + } + return nil +} + +func (x *MessageBody) GetTriggerEventMetadata() *TriggerEventMetadata { + if x != nil { + if x, ok := x.Metadata.(*MessageBody_TriggerEventMetadata); ok { + return x.TriggerEventMetadata + } + } + return nil +} + +func (x *MessageBody) GetCapabilityDonId() uint32 { + if x != nil { + return x.CapabilityDonId + } + return 0 +} + +func (x *MessageBody) GetCallerDonId() uint32 { + if x != nil { + return x.CallerDonId + } + return 0 +} + +func (x *MessageBody) GetCapabilityMethod() string { + if x != nil { + return x.CapabilityMethod + } + return "" +} + +type isMessageBody_Metadata interface { + isMessageBody_Metadata() +} + +type MessageBody_TriggerRegistrationMetadata struct { + TriggerRegistrationMetadata *TriggerRegistrationMetadata `protobuf:"bytes,13,opt,name=trigger_registration_metadata,json=triggerRegistrationMetadata,proto3,oneof"` +} + +type MessageBody_TriggerEventMetadata struct { + TriggerEventMetadata *TriggerEventMetadata `protobuf:"bytes,14,opt,name=trigger_event_metadata,json=triggerEventMetadata,proto3,oneof"` +} + +func (*MessageBody_TriggerRegistrationMetadata) isMessageBody_Metadata() {} + +func (*MessageBody_TriggerEventMetadata) isMessageBody_Metadata() {} + +type TriggerRegistrationMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + LastReceivedEventId string `protobuf:"bytes,1,opt,name=last_received_event_id,json=lastReceivedEventId,proto3" json:"last_received_event_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TriggerRegistrationMetadata) Reset() { + *x = TriggerRegistrationMetadata{} + mi := &file_x_don2don_types_messages_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TriggerRegistrationMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TriggerRegistrationMetadata) ProtoMessage() {} + +func (x *TriggerRegistrationMetadata) ProtoReflect() protoreflect.Message { + mi := &file_x_don2don_types_messages_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TriggerRegistrationMetadata.ProtoReflect.Descriptor instead. +func (*TriggerRegistrationMetadata) Descriptor() ([]byte, []int) { + return file_x_don2don_types_messages_proto_rawDescGZIP(), []int{2} +} + +func (x *TriggerRegistrationMetadata) GetLastReceivedEventId() string { + if x != nil { + return x.LastReceivedEventId + } + return "" +} + +type TriggerEventMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TriggerEventId string `protobuf:"bytes,1,opt,name=trigger_event_id,json=triggerEventId,proto3" json:"trigger_event_id,omitempty"` + WorkflowIds []string `protobuf:"bytes,2,rep,name=workflow_ids,json=workflowIds,proto3" json:"workflow_ids,omitempty"` + TriggerIds []string `protobuf:"bytes,3,rep,name=trigger_ids,json=triggerIds,proto3" json:"trigger_ids,omitempty"` // must be equal length to workflow_ids (or empty if not supported) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TriggerEventMetadata) Reset() { + *x = TriggerEventMetadata{} + mi := &file_x_don2don_types_messages_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TriggerEventMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TriggerEventMetadata) ProtoMessage() {} + +func (x *TriggerEventMetadata) ProtoReflect() protoreflect.Message { + mi := &file_x_don2don_types_messages_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TriggerEventMetadata.ProtoReflect.Descriptor instead. +func (*TriggerEventMetadata) Descriptor() ([]byte, []int) { + return file_x_don2don_types_messages_proto_rawDescGZIP(), []int{3} +} + +func (x *TriggerEventMetadata) GetTriggerEventId() string { + if x != nil { + return x.TriggerEventId + } + return "" +} + +func (x *TriggerEventMetadata) GetWorkflowIds() []string { + if x != nil { + return x.WorkflowIds + } + return nil +} + +func (x *TriggerEventMetadata) GetTriggerIds() []string { + if x != nil { + return x.TriggerIds + } + return nil +} + +var File_x_don2don_types_messages_proto protoreflect.FileDescriptor + +const file_x_don2don_types_messages_proto_rawDesc = "" + + "\n" + + "\x1ex/don2don/types/messages.proto\x12\x06remote\";\n" + + "\aMessage\x12\x1c\n" + + "\tsignature\x18\x01 \x01(\fR\tsignature\x12\x12\n" + + "\x04body\x18\x02 \x01(\fR\x04body\"\x86\x05\n" + + "\vMessageBody\x12\x18\n" + + "\aversion\x18\x01 \x01(\rR\aversion\x12\x16\n" + + "\x06sender\x18\x02 \x01(\fR\x06sender\x12\x1a\n" + + "\breceiver\x18\x03 \x01(\fR\breceiver\x12\x1c\n" + + "\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\x12\x1d\n" + + "\n" + + "message_id\x18\x05 \x01(\fR\tmessageId\x12#\n" + + "\rcapability_id\x18\x06 \x01(\tR\fcapabilityId\x12\x16\n" + + "\x06method\x18\t \x01(\tR\x06method\x12#\n" + + "\x05error\x18\n" + + " \x01(\x0e2\r.remote.ErrorR\x05error\x12\x1a\n" + + "\berrorMsg\x18\v \x01(\tR\berrorMsg\x12\x18\n" + + "\apayload\x18\f \x01(\fR\apayload\x12i\n" + + "\x1dtrigger_registration_metadata\x18\r \x01(\v2#.remote.TriggerRegistrationMetadataH\x00R\x1btriggerRegistrationMetadata\x12T\n" + + "\x16trigger_event_metadata\x18\x0e \x01(\v2\x1c.remote.TriggerEventMetadataH\x00R\x14triggerEventMetadata\x12*\n" + + "\x11capability_don_id\x18\x0f \x01(\rR\x0fcapabilityDonId\x12\"\n" + + "\rcaller_don_id\x18\x10 \x01(\rR\vcallerDonId\x12+\n" + + "\x11capability_method\x18\x11 \x01(\tR\x10capabilityMethodB\n" + + "\n" + + "\bmetadataJ\x04\b\a\x10\bJ\x04\b\b\x10\t\"R\n" + + "\x1bTriggerRegistrationMetadata\x123\n" + + "\x16last_received_event_id\x18\x01 \x01(\tR\x13lastReceivedEventId\"\x84\x01\n" + + "\x14TriggerEventMetadata\x12(\n" + + "\x10trigger_event_id\x18\x01 \x01(\tR\x0etriggerEventId\x12!\n" + + "\fworkflow_ids\x18\x02 \x03(\tR\vworkflowIds\x12\x1f\n" + + "\vtrigger_ids\x18\x03 \x03(\tR\n" + + "triggerIds*v\n" + + "\x05Error\x12\x06\n" + + "\x02OK\x10\x00\x12\x15\n" + + "\x11VALIDATION_FAILED\x10\x01\x12\x18\n" + + "\x14CAPABILITY_NOT_FOUND\x10\x02\x12\x13\n" + + "\x0fINVALID_REQUEST\x10\x03\x12\v\n" + + "\aTIMEOUT\x10\x04\x12\x12\n" + + "\x0eINTERNAL_ERROR\x10\x05B?Z=github.com/smartcontractkit/capabilities/libs/x/don2don/typesb\x06proto3" + +var ( + file_x_don2don_types_messages_proto_rawDescOnce sync.Once + file_x_don2don_types_messages_proto_rawDescData []byte +) + +func file_x_don2don_types_messages_proto_rawDescGZIP() []byte { + file_x_don2don_types_messages_proto_rawDescOnce.Do(func() { + file_x_don2don_types_messages_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_x_don2don_types_messages_proto_rawDesc), len(file_x_don2don_types_messages_proto_rawDesc))) + }) + return file_x_don2don_types_messages_proto_rawDescData +} + +var file_x_don2don_types_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_x_don2don_types_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_x_don2don_types_messages_proto_goTypes = []any{ + (Error)(0), // 0: remote.Error + (*Message)(nil), // 1: remote.Message + (*MessageBody)(nil), // 2: remote.MessageBody + (*TriggerRegistrationMetadata)(nil), // 3: remote.TriggerRegistrationMetadata + (*TriggerEventMetadata)(nil), // 4: remote.TriggerEventMetadata +} +var file_x_don2don_types_messages_proto_depIdxs = []int32{ + 0, // 0: remote.MessageBody.error:type_name -> remote.Error + 3, // 1: remote.MessageBody.trigger_registration_metadata:type_name -> remote.TriggerRegistrationMetadata + 4, // 2: remote.MessageBody.trigger_event_metadata:type_name -> remote.TriggerEventMetadata + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_x_don2don_types_messages_proto_init() } +func file_x_don2don_types_messages_proto_init() { + if File_x_don2don_types_messages_proto != nil { + return + } + file_x_don2don_types_messages_proto_msgTypes[1].OneofWrappers = []any{ + (*MessageBody_TriggerRegistrationMetadata)(nil), + (*MessageBody_TriggerEventMetadata)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_x_don2don_types_messages_proto_rawDesc), len(file_x_don2don_types_messages_proto_rawDesc)), + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_x_don2don_types_messages_proto_goTypes, + DependencyIndexes: file_x_don2don_types_messages_proto_depIdxs, + EnumInfos: file_x_don2don_types_messages_proto_enumTypes, + MessageInfos: file_x_don2don_types_messages_proto_msgTypes, + }.Build() + File_x_don2don_types_messages_proto = out.File + file_x_don2don_types_messages_proto_goTypes = nil + file_x_don2don_types_messages_proto_depIdxs = nil +} diff --git a/libs/x/don2don/types/messages.proto b/libs/x/don2don/types/messages.proto new file mode 100644 index 000000000..425ba7fa7 --- /dev/null +++ b/libs/x/don2don/types/messages.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +option go_package = "github.com/smartcontractkit/capabilities/libs/x/don2don/types"; + +package remote; + +enum Error { + OK = 0; + VALIDATION_FAILED = 1; + CAPABILITY_NOT_FOUND = 2; + INVALID_REQUEST = 3; + TIMEOUT = 4; + INTERNAL_ERROR = 5; +} + +message Message { + bytes signature = 1; + bytes body = 2; // proto-encoded MessageBody to sign +} + +message MessageBody { + reserved 7, 8; + uint32 version = 1; + bytes sender = 2; + bytes receiver = 3; + int64 timestamp = 4; + bytes message_id = 5; // scoped to sender + string capability_id = 6; + string method = 9; // Capability API method ("RegisterTrigger", "Execute", etc) + Error error = 10; + string errorMsg = 11; + + // payload contains a CapabilityRequest or CapabilityResponse + bytes payload = 12; + oneof metadata { + TriggerRegistrationMetadata trigger_registration_metadata = 13; + TriggerEventMetadata trigger_event_metadata = 14; + } + + uint32 capability_don_id = 15; + uint32 caller_don_id = 16; + string capability_method = 17; // method name defined by the capability (empty for legacy "v1" calls) +} + +message TriggerRegistrationMetadata { + string last_received_event_id = 1; +} + +message TriggerEventMetadata { + string trigger_event_id = 1; + repeated string workflow_ids = 2; + repeated string trigger_ids = 3; // must be equal length to workflow_ids (or empty if not supported) +} diff --git a/libs/x/don2don/types/mocks/dispatcher.go b/libs/x/don2don/types/mocks/dispatcher.go new file mode 100644 index 000000000..359913d85 --- /dev/null +++ b/libs/x/don2don/types/mocks/dispatcher.go @@ -0,0 +1,480 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/capabilities/libs/x/don2don/types" +) + +// Dispatcher is an autogenerated mock type for the Dispatcher type +type Dispatcher struct { + mock.Mock +} + +type Dispatcher_Expecter struct { + mock *mock.Mock +} + +func (_m *Dispatcher) EXPECT() *Dispatcher_Expecter { + return &Dispatcher_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with no fields +func (_m *Dispatcher) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Dispatcher_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type Dispatcher_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *Dispatcher_Expecter) Close() *Dispatcher_Close_Call { + return &Dispatcher_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *Dispatcher_Close_Call) Run(run func()) *Dispatcher_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Dispatcher_Close_Call) Return(_a0 error) *Dispatcher_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_Close_Call) RunAndReturn(run func() error) *Dispatcher_Close_Call { + _c.Call.Return(run) + return _c +} + +// HealthReport provides a mock function with no fields +func (_m *Dispatcher) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// Dispatcher_HealthReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HealthReport' +type Dispatcher_HealthReport_Call struct { + *mock.Call +} + +// HealthReport is a helper method to define mock.On call +func (_e *Dispatcher_Expecter) HealthReport() *Dispatcher_HealthReport_Call { + return &Dispatcher_HealthReport_Call{Call: _e.mock.On("HealthReport")} +} + +func (_c *Dispatcher_HealthReport_Call) Run(run func()) *Dispatcher_HealthReport_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Dispatcher_HealthReport_Call) Return(_a0 map[string]error) *Dispatcher_HealthReport_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_HealthReport_Call) RunAndReturn(run func() map[string]error) *Dispatcher_HealthReport_Call { + _c.Call.Return(run) + return _c +} + +// Name provides a mock function with no fields +func (_m *Dispatcher) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Dispatcher_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type Dispatcher_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *Dispatcher_Expecter) Name() *Dispatcher_Name_Call { + return &Dispatcher_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *Dispatcher_Name_Call) Run(run func()) *Dispatcher_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Dispatcher_Name_Call) Return(_a0 string) *Dispatcher_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_Name_Call) RunAndReturn(run func() string) *Dispatcher_Name_Call { + _c.Call.Return(run) + return _c +} + +// Ready provides a mock function with no fields +func (_m *Dispatcher) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Dispatcher_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' +type Dispatcher_Ready_Call struct { + *mock.Call +} + +// Ready is a helper method to define mock.On call +func (_e *Dispatcher_Expecter) Ready() *Dispatcher_Ready_Call { + return &Dispatcher_Ready_Call{Call: _e.mock.On("Ready")} +} + +func (_c *Dispatcher_Ready_Call) Run(run func()) *Dispatcher_Ready_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Dispatcher_Ready_Call) Return(_a0 error) *Dispatcher_Ready_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_Ready_Call) RunAndReturn(run func() error) *Dispatcher_Ready_Call { + _c.Call.Return(run) + return _c +} + +// RemoveReceiver provides a mock function with given fields: capabilityID, donID +func (_m *Dispatcher) RemoveReceiver(capabilityID string, donID uint32) { + _m.Called(capabilityID, donID) +} + +// Dispatcher_RemoveReceiver_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveReceiver' +type Dispatcher_RemoveReceiver_Call struct { + *mock.Call +} + +// RemoveReceiver is a helper method to define mock.On call +// - capabilityID string +// - donID uint32 +func (_e *Dispatcher_Expecter) RemoveReceiver(capabilityID interface{}, donID interface{}) *Dispatcher_RemoveReceiver_Call { + return &Dispatcher_RemoveReceiver_Call{Call: _e.mock.On("RemoveReceiver", capabilityID, donID)} +} + +func (_c *Dispatcher_RemoveReceiver_Call) Run(run func(capabilityID string, donID uint32)) *Dispatcher_RemoveReceiver_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(uint32)) + }) + return _c +} + +func (_c *Dispatcher_RemoveReceiver_Call) Return() *Dispatcher_RemoveReceiver_Call { + _c.Call.Return() + return _c +} + +func (_c *Dispatcher_RemoveReceiver_Call) RunAndReturn(run func(string, uint32)) *Dispatcher_RemoveReceiver_Call { + _c.Run(run) + return _c +} + +// RemoveReceiverForMethod provides a mock function with given fields: capabilityID, donID, method +func (_m *Dispatcher) RemoveReceiverForMethod(capabilityID string, donID uint32, method string) { + _m.Called(capabilityID, donID, method) +} + +// Dispatcher_RemoveReceiverForMethod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveReceiverForMethod' +type Dispatcher_RemoveReceiverForMethod_Call struct { + *mock.Call +} + +// RemoveReceiverForMethod is a helper method to define mock.On call +// - capabilityID string +// - donID uint32 +// - method string +func (_e *Dispatcher_Expecter) RemoveReceiverForMethod(capabilityID interface{}, donID interface{}, method interface{}) *Dispatcher_RemoveReceiverForMethod_Call { + return &Dispatcher_RemoveReceiverForMethod_Call{Call: _e.mock.On("RemoveReceiverForMethod", capabilityID, donID, method)} +} + +func (_c *Dispatcher_RemoveReceiverForMethod_Call) Run(run func(capabilityID string, donID uint32, method string)) *Dispatcher_RemoveReceiverForMethod_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(uint32), args[2].(string)) + }) + return _c +} + +func (_c *Dispatcher_RemoveReceiverForMethod_Call) Return() *Dispatcher_RemoveReceiverForMethod_Call { + _c.Call.Return() + return _c +} + +func (_c *Dispatcher_RemoveReceiverForMethod_Call) RunAndReturn(run func(string, uint32, string)) *Dispatcher_RemoveReceiverForMethod_Call { + _c.Run(run) + return _c +} + +// Send provides a mock function with given fields: peerID, msgBody +func (_m *Dispatcher) Send(peerID ragetypes.PeerID, msgBody *types.MessageBody) error { + ret := _m.Called(peerID, msgBody) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if rf, ok := ret.Get(0).(func(ragetypes.PeerID, *types.MessageBody) error); ok { + r0 = rf(peerID, msgBody) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Dispatcher_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' +type Dispatcher_Send_Call struct { + *mock.Call +} + +// Send is a helper method to define mock.On call +// - peerID ragetypes.PeerID +// - msgBody *types.MessageBody +func (_e *Dispatcher_Expecter) Send(peerID interface{}, msgBody interface{}) *Dispatcher_Send_Call { + return &Dispatcher_Send_Call{Call: _e.mock.On("Send", peerID, msgBody)} +} + +func (_c *Dispatcher_Send_Call) Run(run func(peerID ragetypes.PeerID, msgBody *types.MessageBody)) *Dispatcher_Send_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ragetypes.PeerID), args[1].(*types.MessageBody)) + }) + return _c +} + +func (_c *Dispatcher_Send_Call) Return(_a0 error) *Dispatcher_Send_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_Send_Call) RunAndReturn(run func(ragetypes.PeerID, *types.MessageBody) error) *Dispatcher_Send_Call { + _c.Call.Return(run) + return _c +} + +// SetReceiver provides a mock function with given fields: capabilityID, donID, receiver +func (_m *Dispatcher) SetReceiver(capabilityID string, donID uint32, receiver types.Receiver) error { + ret := _m.Called(capabilityID, donID, receiver) + + if len(ret) == 0 { + panic("no return value specified for SetReceiver") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, uint32, types.Receiver) error); ok { + r0 = rf(capabilityID, donID, receiver) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Dispatcher_SetReceiver_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReceiver' +type Dispatcher_SetReceiver_Call struct { + *mock.Call +} + +// SetReceiver is a helper method to define mock.On call +// - capabilityID string +// - donID uint32 +// - receiver types.Receiver +func (_e *Dispatcher_Expecter) SetReceiver(capabilityID interface{}, donID interface{}, receiver interface{}) *Dispatcher_SetReceiver_Call { + return &Dispatcher_SetReceiver_Call{Call: _e.mock.On("SetReceiver", capabilityID, donID, receiver)} +} + +func (_c *Dispatcher_SetReceiver_Call) Run(run func(capabilityID string, donID uint32, receiver types.Receiver)) *Dispatcher_SetReceiver_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(uint32), args[2].(types.Receiver)) + }) + return _c +} + +func (_c *Dispatcher_SetReceiver_Call) Return(_a0 error) *Dispatcher_SetReceiver_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_SetReceiver_Call) RunAndReturn(run func(string, uint32, types.Receiver) error) *Dispatcher_SetReceiver_Call { + _c.Call.Return(run) + return _c +} + +// SetReceiverForMethod provides a mock function with given fields: capabilityID, donID, method, receiver +func (_m *Dispatcher) SetReceiverForMethod(capabilityID string, donID uint32, method string, receiver types.Receiver) error { + ret := _m.Called(capabilityID, donID, method, receiver) + + if len(ret) == 0 { + panic("no return value specified for SetReceiverForMethod") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, uint32, string, types.Receiver) error); ok { + r0 = rf(capabilityID, donID, method, receiver) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Dispatcher_SetReceiverForMethod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReceiverForMethod' +type Dispatcher_SetReceiverForMethod_Call struct { + *mock.Call +} + +// SetReceiverForMethod is a helper method to define mock.On call +// - capabilityID string +// - donID uint32 +// - method string +// - receiver types.Receiver +func (_e *Dispatcher_Expecter) SetReceiverForMethod(capabilityID interface{}, donID interface{}, method interface{}, receiver interface{}) *Dispatcher_SetReceiverForMethod_Call { + return &Dispatcher_SetReceiverForMethod_Call{Call: _e.mock.On("SetReceiverForMethod", capabilityID, donID, method, receiver)} +} + +func (_c *Dispatcher_SetReceiverForMethod_Call) Run(run func(capabilityID string, donID uint32, method string, receiver types.Receiver)) *Dispatcher_SetReceiverForMethod_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(uint32), args[2].(string), args[3].(types.Receiver)) + }) + return _c +} + +func (_c *Dispatcher_SetReceiverForMethod_Call) Return(_a0 error) *Dispatcher_SetReceiverForMethod_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_SetReceiverForMethod_Call) RunAndReturn(run func(string, uint32, string, types.Receiver) error) *Dispatcher_SetReceiverForMethod_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: _a0 +func (_m *Dispatcher) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Dispatcher_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type Dispatcher_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +// - _a0 context.Context +func (_e *Dispatcher_Expecter) Start(_a0 interface{}) *Dispatcher_Start_Call { + return &Dispatcher_Start_Call{Call: _e.mock.On("Start", _a0)} +} + +func (_c *Dispatcher_Start_Call) Run(run func(_a0 context.Context)) *Dispatcher_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Dispatcher_Start_Call) Return(_a0 error) *Dispatcher_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Dispatcher_Start_Call) RunAndReturn(run func(context.Context) error) *Dispatcher_Start_Call { + _c.Call.Return(run) + return _c +} + +// NewDispatcher creates a new instance of Dispatcher. 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 NewDispatcher(t interface { + mock.TestingT + Cleanup(func()) +}) *Dispatcher { + mock := &Dispatcher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/libs/x/don2don/types/mocks/receiver.go b/libs/x/don2don/types/mocks/receiver.go new file mode 100644 index 000000000..9b68c1ad7 --- /dev/null +++ b/libs/x/don2don/types/mocks/receiver.go @@ -0,0 +1,72 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/capabilities/libs/x/don2don/types" +) + +// Receiver is an autogenerated mock type for the Receiver type +type Receiver struct { + mock.Mock +} + +type Receiver_Expecter struct { + mock *mock.Mock +} + +func (_m *Receiver) EXPECT() *Receiver_Expecter { + return &Receiver_Expecter{mock: &_m.Mock} +} + +// Receive provides a mock function with given fields: ctx, msg +func (_m *Receiver) Receive(ctx context.Context, msg *types.MessageBody) { + _m.Called(ctx, msg) +} + +// Receiver_Receive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Receive' +type Receiver_Receive_Call struct { + *mock.Call +} + +// Receive is a helper method to define mock.On call +// - ctx context.Context +// - msg *types.MessageBody +func (_e *Receiver_Expecter) Receive(ctx interface{}, msg interface{}) *Receiver_Receive_Call { + return &Receiver_Receive_Call{Call: _e.mock.On("Receive", ctx, msg)} +} + +func (_c *Receiver_Receive_Call) Run(run func(ctx context.Context, msg *types.MessageBody)) *Receiver_Receive_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.MessageBody)) + }) + return _c +} + +func (_c *Receiver_Receive_Call) Return() *Receiver_Receive_Call { + _c.Call.Return() + return _c +} + +func (_c *Receiver_Receive_Call) RunAndReturn(run func(context.Context, *types.MessageBody)) *Receiver_Receive_Call { + _c.Run(run) + return _c +} + +// NewReceiver creates a new instance of Receiver. 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 NewReceiver(t interface { + mock.TestingT + Cleanup(func()) +}) *Receiver { + mock := &Receiver{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/libs/x/don2don/types/types.go b/libs/x/don2don/types/types.go new file mode 100644 index 000000000..0063f45b7 --- /dev/null +++ b/libs/x/don2don/types/types.go @@ -0,0 +1,66 @@ +// Package types holds the DON-to-DON wire messages and the interfaces the layers above them speak: +// the Dispatcher a message is sent through, the Receiver it is delivered to, and the aggregator and +// hasher used to agree on responses. +// +// The proto keeps its original package name (remote) and field numbers, so the wire is unchanged by +// the move out of chainlink's core/capabilities/remote/types - only the Go import path differs. +// Nothing may generate a second copy of it: two registrations of don2don.MessageBody in one binary +// panic at init, so core imports this rather than keeping its own. +// +// The proto is compiled from the module root so the descriptor is registered under +// x/don2don/types/messages.proto rather than a bare messages.proto: descriptor file names are +// global, and a bare one collides with every other messages.proto a binary happens to link in. +// +//go:generate protoc -I ../../.. --go_out=../../.. --go_opt=paths=source_relative x/don2don/types/messages.proto +package types + +import ( + "context" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +const ( + MethodRegisterTrigger = "RegisterTrigger" + MethodUnregisterTrigger = "UnregisterTrigger" + MethodTriggerRegistrationCheck = "TriggerRegistrationCheck" + MethodTriggerEvent = "TriggerEvent" + MethodExecute = "Execute" + MethodTriggerEventAck = "TriggerEventACK" +) + +type Dispatcher interface { + services.Service + SetReceiver(capabilityID string, donID uint32, receiver Receiver) error + RemoveReceiver(capabilityID string, donID uint32) + SetReceiverForMethod(capabilityID string, donID uint32, method string, receiver Receiver) error + RemoveReceiverForMethod(capabilityID string, donID uint32, method string) + Send(peerID rage.PeerID, msgBody *MessageBody) error +} + +type Receiver interface { + Receive(ctx context.Context, msg *MessageBody) +} + +type ReceiverService interface { + services.Service + Receiver +} + +type Aggregator interface { + Aggregate(eventID string, responses [][]byte) (commoncap.TriggerResponse, error) +} + +// NOTE: this type will become part of the Registry (KS-108) +type DON struct { + ID string + Members []rage.PeerID + F uint8 +} + +type MessageHasher interface { + Hash(msg *MessageBody) ([32]byte, error) +} diff --git a/libs/x/don2don/utils.go b/libs/x/don2don/utils.go new file mode 100644 index 000000000..f542ca02a --- /dev/null +++ b/libs/x/don2don/utils.go @@ -0,0 +1,70 @@ +package don2don + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "errors" + "fmt" + "unicode" + + "google.golang.org/protobuf/proto" + + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +const ( + maxLoggedStringLen = 256 +) + +func ValidateMessage(msg *rage.Message, expectedReceiver rage.PeerID) (*don2dontypes.MessageBody, error) { + var topLevelMessage don2dontypes.Message + err := proto.Unmarshal(msg.Payload, &topLevelMessage) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal message, err: %w", err) + } + var body don2dontypes.MessageBody + err = proto.Unmarshal(topLevelMessage.Body, &body) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal message body, err: %w", err) + } + if len(body.Sender) != rage.PeerIDLength || len(body.Receiver) != rage.PeerIDLength { + return &body, fmt.Errorf("invalid sender length (%d) or receiver length (%d)", len(body.Sender), len(body.Receiver)) + } + if !ed25519.Verify(body.Sender, topLevelMessage.Body, topLevelMessage.Signature) { + return &body, errors.New("failed to verify message signature") + } + // NOTE we currently don't support relaying messages so the p2p message sender needs to be the message author + if !bytes.Equal(body.Sender, msg.Sender[:]) { + return &body, errors.New("sender in message body does not match sender of p2p message") + } + if !bytes.Equal(body.Receiver, expectedReceiver[:]) { + return &body, errors.New("receiver in message body does not match expected receiver") + } + return &body, nil +} + +func ToPeerID(peerID []byte) (rage.PeerID, error) { + if len(peerID) != rage.PeerIDLength { + return rage.PeerID{}, fmt.Errorf("invalid peer ID length: %d", len(peerID)) + } + + var id rage.PeerID + copy(id[:], peerID) + return id, nil +} + +func SanitizeLogString(s string) string { + tooLongSuffix := "" + if len(s) > maxLoggedStringLen { + s = s[:maxLoggedStringLen] + tooLongSuffix = " [TRUNCATED]" + } + for i := 0; i < len(s); i++ { + if !unicode.IsPrint(rune(s[i])) { + return "[UNPRINTABLE] " + hex.EncodeToString([]byte(s)) + tooLongSuffix + } + } + return s + tooLongSuffix +} diff --git a/libs/x/don2don/utils_test.go b/libs/x/don2don/utils_test.go new file mode 100644 index 000000000..deabf4e96 --- /dev/null +++ b/libs/x/don2don/utils_test.go @@ -0,0 +1,120 @@ +package don2don_test + +import ( + "crypto/ed25519" + "crypto/rand" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/capabilities/libs/x/don2don" + don2dontypes "github.com/smartcontractkit/capabilities/libs/x/don2don/types" + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +const ( + capID1 = "cap1" + capID2 = "cap2" + donID1 = uint32(1) + payload1 = "hello world" + payload2 = "goodbye world" +) + +func TestValidateMessage(t *testing.T) { + t.Parallel() + + privKey1, peerID1 := newKeyPair(t) + _, peerID2 := newKeyPair(t) + + // valid + p2pMsg := encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload1)) + body, err := don2don.ValidateMessage(&p2pMsg, peerID2) + require.NoError(t, err) + require.Equal(t, peerID1[:], body.Sender) + require.Equal(t, payload1, string(body.Payload)) + + // invalid sender + p2pMsg = encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload1)) + p2pMsg.Sender = peerID2 + _, err = don2don.ValidateMessage(&p2pMsg, peerID2) + require.Error(t, err) + + // invalid receiver + p2pMsg = encodeAndSign(t, privKey1, peerID1, peerID2, capID1, donID1, []byte(payload1)) + _, err = don2don.ValidateMessage(&p2pMsg, peerID1) + require.Error(t, err) +} + +func newKeyPair(t *testing.T) (ed25519.PrivateKey, ragetypes.PeerID) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + peerID, err := ragetypes.PeerIDFromPrivateKey(privKey) + require.NoError(t, err) + return privKey, peerID +} + +func encodeAndSign(t *testing.T, senderPrivKey ed25519.PrivateKey, senderID rage.PeerID, receiverID rage.PeerID, capabilityID string, donID uint32, payload []byte) rage.Message { + body := &don2dontypes.MessageBody{ + Sender: senderID[:], + Receiver: receiverID[:], + CapabilityId: capabilityID, + CapabilityDonId: donID, + Payload: payload, + } + return signBody(t, senderPrivKey, body, senderID) +} + +func encodeAndSignForMethod(t *testing.T, senderPrivKey ed25519.PrivateKey, senderID rage.PeerID, receiverID rage.PeerID, capabilityID string, methodName string, donID uint32, payload []byte) rage.Message { + body := &don2dontypes.MessageBody{ + Sender: senderID[:], + Receiver: receiverID[:], + CapabilityId: capabilityID, + CapabilityMethod: methodName, + CapabilityDonId: donID, + Payload: payload, + } + return signBody(t, senderPrivKey, body, senderID) +} + +func signBody(t *testing.T, senderPrivKey ed25519.PrivateKey, body *don2dontypes.MessageBody, senderID rage.PeerID) rage.Message { + rawBody, err := proto.Marshal(body) + require.NoError(t, err) + signature := ed25519.Sign(senderPrivKey, rawBody) + + msg := don2dontypes.Message{ + Signature: signature, + Body: rawBody, + } + rawMsg, err := proto.Marshal(&msg) + require.NoError(t, err) + + return rage.Message{ + Sender: senderID, + Payload: rawMsg, + } +} + +func TestToPeerID(t *testing.T) { + t.Parallel() + + id, err := don2don.ToPeerID([]byte("12345678901234567890123456789012")) + require.NoError(t, err) + require.Equal(t, "12D3KooWD8QYTQVYjB6oog4Ej8PcPpqTrPRnxLQap8yY8KUQRVvq", id.String()) +} + +func TestSanitizeLogString(t *testing.T) { + t.Parallel() + + require.Equal(t, "hello", don2don.SanitizeLogString("hello")) + require.Equal(t, "[UNPRINTABLE] 0a", don2don.SanitizeLogString("\n")) + + var longString strings.Builder + for range 100 { + longString.WriteString("aa-aa-aa-") + } + require.Equal(t, longString.String()[:256]+" [TRUNCATED]", don2don.SanitizeLogString(longString.String())) +} diff --git a/libs/x/don2don/validation/validation.go b/libs/x/don2don/validation/validation.go new file mode 100644 index 000000000..67ee3a504 --- /dev/null +++ b/libs/x/don2don/validation/validation.go @@ -0,0 +1,38 @@ +package validation + +import ( + "encoding/hex" + "errors" + "unicode" +) + +const ( + validWorkflowIDLen = 64 + maxIDLen = 128 +) + +// Workflow IDs and Execution IDs are 32-byte hex-encoded strings +func ValidateWorkflowOrExecutionID(id string) error { + if len(id) != validWorkflowIDLen { + return errors.New("must be 32 bytes long") + } + _, err := hex.DecodeString(id) + if err != nil { + return errors.New("must be a hex-encoded string") + } + + return nil +} + +// Trigger event IDs and message IDs can only contain printable characters and must be non-empty +func IsValidID(id string) bool { + if len(id) == 0 || len(id) > maxIDLen { + return false + } + for i := 0; i < len(id); i++ { + if !unicode.IsPrint(rune(id[i])) { + return false + } + } + return true +} diff --git a/libs/x/don2don/validation/validation_test.go b/libs/x/don2don/validation/validation_test.go new file mode 100644 index 000000000..5429e040b --- /dev/null +++ b/libs/x/don2don/validation/validation_test.go @@ -0,0 +1,19 @@ +package validation + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsValidWorkflowID(t *testing.T) { + require.Error(t, ValidateWorkflowOrExecutionID("too_short")) + require.Error(t, ValidateWorkflowOrExecutionID("nothex--95ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0")) + require.NoError(t, ValidateWorkflowOrExecutionID("15c631d295ef5e32deb99a10ee6804bc4af13855687559d7ff6552ac6dbb2ce0")) +} + +func TestIsValidTriggerEventID(t *testing.T) { + require.False(t, IsValidID("")) + require.False(t, IsValidID("\n\n")) + require.True(t, IsValidID("id_id_2")) +} diff --git a/libs/x/internal/synctest/synctest.go b/libs/x/internal/synctest/synctest.go new file mode 100644 index 000000000..f2191b338 --- /dev/null +++ b/libs/x/internal/synctest/synctest.go @@ -0,0 +1,32 @@ +//go:build !race + +// Package synctest is a wrapper around the testing/synctest package to handle a known bug in Go's race detection when dealing with `synctest.Test`. +package synctest + +import ( + "testing" + "testing/synctest" + "time" +) + +// Test wraps `testing/synctest.Test`. +// In !race builds we enable the synctest bubble; under -race this is a no-op via synctest_race.go +// due to https://github.com/golang/go/issues/76691 (fixed in Go 1.27). +func Test(t *testing.T, f func(*testing.T)) { + synctest.Test(t, f) +} + +// Wait wraps `testing/synctest.Wait` (no-op under -race; see synctest_race.go). +func Wait() { + synctest.Wait() +} + +// SlowDelay returns a slow time duration for use in synctest tests. +func SlowDelay() time.Duration { + return 1 * time.Minute +} + +// FastDelay returns a fast time duration for use in synctest tests. +func FastDelay() time.Duration { + return 1 * time.Second +} diff --git a/libs/x/internal/synctest/synctest_race.go b/libs/x/internal/synctest/synctest_race.go new file mode 100644 index 000000000..9c1321269 --- /dev/null +++ b/libs/x/internal/synctest/synctest_race.go @@ -0,0 +1,30 @@ +//go:build race + +package synctest + +import ( + "testing" + "time" +) + +// Test wraps `testing/synctest.Test` to disable the synctest bubble when `-race` is enabled. +// There is a known issue with `synctest` when run with the `-race` detector that causes false positives. +// This should be fixed in Go 1.27 and we can remove this package. +// https://github.com/golang/go/issues/76691 +func Test(t *testing.T, f func(*testing.T)) { + f(t) +} + +// Wait wraps `testing/synctest.Wait` to disable the synctest bubble when `-race` is enabled. +func Wait() { +} + +// SlowDelay returns a slow time duration for use in synctest tests. +func SlowDelay() time.Duration { + return 500 * time.Millisecond +} + +// FastDelay returns a fast time duration for use in synctest tests. +func FastDelay() time.Duration { + return 10 * time.Millisecond +} diff --git a/libs/x/rage/counter.go b/libs/x/rage/counter.go new file mode 100644 index 000000000..b9fed6b1a --- /dev/null +++ b/libs/x/rage/counter.go @@ -0,0 +1,23 @@ +package rage + +import ( + "encoding/binary" +) + +// counter is a simple abstraction that can be used to generate unique peer group IDs. +type counter struct { + x uint64 +} + +// Bytes returns the counter as a 32-byte array. +func (g *counter) Bytes() [32]byte { + var b [32]byte + binary.BigEndian.PutUint64(b[24:], g.x) + return b +} + +// Inc increments the counter. +func (g *counter) Inc() *counter { + g.x++ + return g +} diff --git a/libs/x/rage/counter_test.go b/libs/x/rage/counter_test.go new file mode 100644 index 000000000..ee25b7712 --- /dev/null +++ b/libs/x/rage/counter_test.go @@ -0,0 +1,16 @@ +package rage + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_groupID(t *testing.T) { + g := &counter{} + assert.Equal(t, [32]byte{}, g.Bytes()) + g.Inc() + assert.Equal(t, [32]byte{31: 1}, g.Bytes()) + g.Inc() + assert.Equal(t, [32]byte{31: 2}, g.Bytes()) +} diff --git a/libs/x/rage/keyring.go b/libs/x/rage/keyring.go new file mode 100644 index 000000000..4a88b6108 --- /dev/null +++ b/libs/x/rage/keyring.go @@ -0,0 +1,50 @@ +package rage + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" +) + +// NewSignerPeerKeyring adapts a crypto.Signer holding the node's P2P key into the keyring a rage +// peer signs with. Moved here from core's ocrcommon, which a process that only speaks DON-to-DON +// has no other reason to depend on. +func NewSignerPeerKeyring(signer crypto.Signer) (ragetypes.PeerKeyring, error) { + peerPublicKey, err := ragetypes.PeerPublicKeyFromGenericPublicKey(signer.Public()) + if err != nil { + return nil, err + } + return &signerPeerKeyring{signer: signer, peerPublicKey: peerPublicKey}, nil +} + +type signerPeerKeyring struct { + signer crypto.Signer + peerPublicKey ragetypes.PeerPublicKey +} + +var _ ragetypes.PeerKeyring = (*signerPeerKeyring)(nil) + +func (s *signerPeerKeyring) PublicKey() ragetypes.PeerPublicKey { return s.peerPublicKey } + +// Sign produces an EdDSA-Ed25519 signature, as PeerKeyring requires: the key is ed25519, so the +// message is signed whole rather than pre-hashed. +func (s *signerPeerKeyring) Sign(msg []byte) ([]byte, error) { + return s.signer.Sign(rand.Reader, msg, crypto.Hash(0)) +} + +// MustNewPeerID returns the peer ID of a freshly generated key, panicking if one cannot be made. +// For tests and local tooling that need a peer ID and do not care which; moved here from core's +// utils package, which has nothing else this needs. +func MustNewPeerID() string { + pubKey, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + peerID, err := ragetypes.PeerIDFromPublicKey(pubKey) + if err != nil { + panic(err) + } + return peerID.String() +} diff --git a/libs/x/rage/metrics.go b/libs/x/rage/metrics.go new file mode 100644 index 000000000..0b1da3a1b --- /dev/null +++ b/libs/x/rage/metrics.go @@ -0,0 +1,44 @@ +package rage + +import ( + "fmt" + + "go.opentelemetry.io/otel/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" +) + +type SharedPeerMetrics struct { + // Measure current number of PeerGroups. On non-bootstrap nodes, the number of discovery groups + // should be at least equal to number of remote DONs this node is expected to connect with. + // The number of messaging groups should be equal to number of remote nodes this node is expected to connect with. + discoveryGroups metric.Int64Gauge + messagingGroups metric.Int64Gauge + + // Failure count and latency of a peer group update call. + groupUpdateFailureCounter metric.Int64Counter + groupUpdateDurationMs metric.Int64Histogram +} + +func initSharedPeerMetrics() (m *SharedPeerMetrics, err error) { + m = &SharedPeerMetrics{} + m.discoveryGroups, err = beholder.GetMeter().Int64Gauge("platform_don2don_discovery_groups") + if err != nil { + return nil, fmt.Errorf("failed to register platform_don2don_discovery_groups gauge: %w", err) + } + m.messagingGroups, err = beholder.GetMeter().Int64Gauge("platform_don2don_messaging_groups") + if err != nil { + return nil, fmt.Errorf("failed to register platform_don2don_messaging_groups gauge: %w", err) + } + m.groupUpdateFailureCounter, err = beholder.GetMeter().Int64Counter("platform_don2don_group_update_failure_total") + if err != nil { + return nil, fmt.Errorf("failed to register platform_don2don_group_update_failure_total counter: %w", err) + } + m.groupUpdateDurationMs, err = beholder.GetMeter().Int64Histogram( + "platform_don2don_group_update_duration_ms", + metric.WithUnit("ms")) + if err != nil { + return nil, fmt.Errorf("failed to register platform_don2don_group_update_duration_ms histogram: %w", err) + } + return m, nil +} diff --git a/libs/x/rage/mocks/peer.go b/libs/x/rage/mocks/peer.go new file mode 100644 index 000000000..ac9fbb6c5 --- /dev/null +++ b/libs/x/rage/mocks/peer.go @@ -0,0 +1,499 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + mock "github.com/stretchr/testify/mock" + + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// Peer is an autogenerated mock type for the Peer type +type Peer struct { + mock.Mock +} + +type Peer_Expecter struct { + mock *mock.Mock +} + +func (_m *Peer) EXPECT() *Peer_Expecter { + return &Peer_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with no fields +func (_m *Peer) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Peer_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type Peer_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *Peer_Expecter) Close() *Peer_Close_Call { + return &Peer_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *Peer_Close_Call) Run(run func()) *Peer_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Peer_Close_Call) Return(_a0 error) *Peer_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_Close_Call) RunAndReturn(run func() error) *Peer_Close_Call { + _c.Call.Return(run) + return _c +} + +// HealthReport provides a mock function with no fields +func (_m *Peer) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// Peer_HealthReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HealthReport' +type Peer_HealthReport_Call struct { + *mock.Call +} + +// HealthReport is a helper method to define mock.On call +func (_e *Peer_Expecter) HealthReport() *Peer_HealthReport_Call { + return &Peer_HealthReport_Call{Call: _e.mock.On("HealthReport")} +} + +func (_c *Peer_HealthReport_Call) Run(run func()) *Peer_HealthReport_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Peer_HealthReport_Call) Return(_a0 map[string]error) *Peer_HealthReport_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_HealthReport_Call) RunAndReturn(run func() map[string]error) *Peer_HealthReport_Call { + _c.Call.Return(run) + return _c +} + +// ID provides a mock function with no fields +func (_m *Peer) ID() ragetypes.PeerID { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ID") + } + + var r0 ragetypes.PeerID + if rf, ok := ret.Get(0).(func() ragetypes.PeerID); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ragetypes.PeerID) + } + } + + return r0 +} + +// Peer_ID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ID' +type Peer_ID_Call struct { + *mock.Call +} + +// ID is a helper method to define mock.On call +func (_e *Peer_Expecter) ID() *Peer_ID_Call { + return &Peer_ID_Call{Call: _e.mock.On("ID")} +} + +func (_c *Peer_ID_Call) Run(run func()) *Peer_ID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Peer_ID_Call) Return(_a0 ragetypes.PeerID) *Peer_ID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_ID_Call) RunAndReturn(run func() ragetypes.PeerID) *Peer_ID_Call { + _c.Call.Return(run) + return _c +} + +// IsBootstrap provides a mock function with no fields +func (_m *Peer) IsBootstrap() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsBootstrap") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Peer_IsBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsBootstrap' +type Peer_IsBootstrap_Call struct { + *mock.Call +} + +// IsBootstrap is a helper method to define mock.On call +func (_e *Peer_Expecter) IsBootstrap() *Peer_IsBootstrap_Call { + return &Peer_IsBootstrap_Call{Call: _e.mock.On("IsBootstrap")} +} + +func (_c *Peer_IsBootstrap_Call) Run(run func()) *Peer_IsBootstrap_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Peer_IsBootstrap_Call) Return(_a0 bool) *Peer_IsBootstrap_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_IsBootstrap_Call) RunAndReturn(run func() bool) *Peer_IsBootstrap_Call { + _c.Call.Return(run) + return _c +} + +// Name provides a mock function with no fields +func (_m *Peer) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Peer_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type Peer_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *Peer_Expecter) Name() *Peer_Name_Call { + return &Peer_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *Peer_Name_Call) Run(run func()) *Peer_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Peer_Name_Call) Return(_a0 string) *Peer_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_Name_Call) RunAndReturn(run func() string) *Peer_Name_Call { + _c.Call.Return(run) + return _c +} + +// Ready provides a mock function with no fields +func (_m *Peer) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Peer_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' +type Peer_Ready_Call struct { + *mock.Call +} + +// Ready is a helper method to define mock.On call +func (_e *Peer_Expecter) Ready() *Peer_Ready_Call { + return &Peer_Ready_Call{Call: _e.mock.On("Ready")} +} + +func (_c *Peer_Ready_Call) Run(run func()) *Peer_Ready_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Peer_Ready_Call) Return(_a0 error) *Peer_Ready_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_Ready_Call) RunAndReturn(run func() error) *Peer_Ready_Call { + _c.Call.Return(run) + return _c +} + +// Receive provides a mock function with no fields +func (_m *Peer) Receive() <-chan rage.Message { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Receive") + } + + var r0 <-chan rage.Message + if rf, ok := ret.Get(0).(func() <-chan rage.Message); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan rage.Message) + } + } + + return r0 +} + +// Peer_Receive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Receive' +type Peer_Receive_Call struct { + *mock.Call +} + +// Receive is a helper method to define mock.On call +func (_e *Peer_Expecter) Receive() *Peer_Receive_Call { + return &Peer_Receive_Call{Call: _e.mock.On("Receive")} +} + +func (_c *Peer_Receive_Call) Run(run func()) *Peer_Receive_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Peer_Receive_Call) Return(_a0 <-chan rage.Message) *Peer_Receive_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_Receive_Call) RunAndReturn(run func() <-chan rage.Message) *Peer_Receive_Call { + _c.Call.Return(run) + return _c +} + +// Send provides a mock function with given fields: peerID, msg +func (_m *Peer) Send(peerID ragetypes.PeerID, msg []byte) error { + ret := _m.Called(peerID, msg) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if rf, ok := ret.Get(0).(func(ragetypes.PeerID, []byte) error); ok { + r0 = rf(peerID, msg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Peer_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' +type Peer_Send_Call struct { + *mock.Call +} + +// Send is a helper method to define mock.On call +// - peerID ragetypes.PeerID +// - msg []byte +func (_e *Peer_Expecter) Send(peerID interface{}, msg interface{}) *Peer_Send_Call { + return &Peer_Send_Call{Call: _e.mock.On("Send", peerID, msg)} +} + +func (_c *Peer_Send_Call) Run(run func(peerID ragetypes.PeerID, msg []byte)) *Peer_Send_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ragetypes.PeerID), args[1].([]byte)) + }) + return _c +} + +func (_c *Peer_Send_Call) Return(_a0 error) *Peer_Send_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_Send_Call) RunAndReturn(run func(ragetypes.PeerID, []byte) error) *Peer_Send_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: _a0 +func (_m *Peer) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Peer_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type Peer_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +// - _a0 context.Context +func (_e *Peer_Expecter) Start(_a0 interface{}) *Peer_Start_Call { + return &Peer_Start_Call{Call: _e.mock.On("Start", _a0)} +} + +func (_c *Peer_Start_Call) Run(run func(_a0 context.Context)) *Peer_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Peer_Start_Call) Return(_a0 error) *Peer_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_Start_Call) RunAndReturn(run func(context.Context) error) *Peer_Start_Call { + _c.Call.Return(run) + return _c +} + +// UpdateConnections provides a mock function with given fields: peers +func (_m *Peer) UpdateConnections(peers map[ragetypes.PeerID]rage.StreamConfig) error { + ret := _m.Called(peers) + + if len(ret) == 0 { + panic("no return value specified for UpdateConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[ragetypes.PeerID]rage.StreamConfig) error); ok { + r0 = rf(peers) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Peer_UpdateConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateConnections' +type Peer_UpdateConnections_Call struct { + *mock.Call +} + +// UpdateConnections is a helper method to define mock.On call +// - peers map[ragetypes.PeerID]rage.StreamConfig +func (_e *Peer_Expecter) UpdateConnections(peers interface{}) *Peer_UpdateConnections_Call { + return &Peer_UpdateConnections_Call{Call: _e.mock.On("UpdateConnections", peers)} +} + +func (_c *Peer_UpdateConnections_Call) Run(run func(peers map[ragetypes.PeerID]rage.StreamConfig)) *Peer_UpdateConnections_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[ragetypes.PeerID]rage.StreamConfig)) + }) + return _c +} + +func (_c *Peer_UpdateConnections_Call) Return(_a0 error) *Peer_UpdateConnections_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Peer_UpdateConnections_Call) RunAndReturn(run func(map[ragetypes.PeerID]rage.StreamConfig) error) *Peer_UpdateConnections_Call { + _c.Call.Return(run) + return _c +} + +// NewPeer creates a new instance of Peer. 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 NewPeer(t interface { + mock.TestingT + Cleanup(func()) +}) *Peer { + mock := &Peer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/libs/x/rage/mocks/peer_wrapper.go b/libs/x/rage/mocks/peer_wrapper.go new file mode 100644 index 000000000..81d834872 --- /dev/null +++ b/libs/x/rage/mocks/peer_wrapper.go @@ -0,0 +1,313 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// PeerWrapper is an autogenerated mock type for the PeerWrapper type +type PeerWrapper struct { + mock.Mock +} + +type PeerWrapper_Expecter struct { + mock *mock.Mock +} + +func (_m *PeerWrapper) EXPECT() *PeerWrapper_Expecter { + return &PeerWrapper_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with no fields +func (_m *PeerWrapper) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// PeerWrapper_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type PeerWrapper_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *PeerWrapper_Expecter) Close() *PeerWrapper_Close_Call { + return &PeerWrapper_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *PeerWrapper_Close_Call) Run(run func()) *PeerWrapper_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *PeerWrapper_Close_Call) Return(_a0 error) *PeerWrapper_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PeerWrapper_Close_Call) RunAndReturn(run func() error) *PeerWrapper_Close_Call { + _c.Call.Return(run) + return _c +} + +// GetPeer provides a mock function with no fields +func (_m *PeerWrapper) GetPeer() rage.Peer { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetPeer") + } + + var r0 rage.Peer + if rf, ok := ret.Get(0).(func() rage.Peer); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rage.Peer) + } + } + + return r0 +} + +// PeerWrapper_GetPeer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPeer' +type PeerWrapper_GetPeer_Call struct { + *mock.Call +} + +// GetPeer is a helper method to define mock.On call +func (_e *PeerWrapper_Expecter) GetPeer() *PeerWrapper_GetPeer_Call { + return &PeerWrapper_GetPeer_Call{Call: _e.mock.On("GetPeer")} +} + +func (_c *PeerWrapper_GetPeer_Call) Run(run func()) *PeerWrapper_GetPeer_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *PeerWrapper_GetPeer_Call) Return(_a0 rage.Peer) *PeerWrapper_GetPeer_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PeerWrapper_GetPeer_Call) RunAndReturn(run func() rage.Peer) *PeerWrapper_GetPeer_Call { + _c.Call.Return(run) + return _c +} + +// HealthReport provides a mock function with no fields +func (_m *PeerWrapper) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// PeerWrapper_HealthReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HealthReport' +type PeerWrapper_HealthReport_Call struct { + *mock.Call +} + +// HealthReport is a helper method to define mock.On call +func (_e *PeerWrapper_Expecter) HealthReport() *PeerWrapper_HealthReport_Call { + return &PeerWrapper_HealthReport_Call{Call: _e.mock.On("HealthReport")} +} + +func (_c *PeerWrapper_HealthReport_Call) Run(run func()) *PeerWrapper_HealthReport_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *PeerWrapper_HealthReport_Call) Return(_a0 map[string]error) *PeerWrapper_HealthReport_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PeerWrapper_HealthReport_Call) RunAndReturn(run func() map[string]error) *PeerWrapper_HealthReport_Call { + _c.Call.Return(run) + return _c +} + +// Name provides a mock function with no fields +func (_m *PeerWrapper) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// PeerWrapper_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type PeerWrapper_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *PeerWrapper_Expecter) Name() *PeerWrapper_Name_Call { + return &PeerWrapper_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *PeerWrapper_Name_Call) Run(run func()) *PeerWrapper_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *PeerWrapper_Name_Call) Return(_a0 string) *PeerWrapper_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PeerWrapper_Name_Call) RunAndReturn(run func() string) *PeerWrapper_Name_Call { + _c.Call.Return(run) + return _c +} + +// Ready provides a mock function with no fields +func (_m *PeerWrapper) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// PeerWrapper_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' +type PeerWrapper_Ready_Call struct { + *mock.Call +} + +// Ready is a helper method to define mock.On call +func (_e *PeerWrapper_Expecter) Ready() *PeerWrapper_Ready_Call { + return &PeerWrapper_Ready_Call{Call: _e.mock.On("Ready")} +} + +func (_c *PeerWrapper_Ready_Call) Run(run func()) *PeerWrapper_Ready_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *PeerWrapper_Ready_Call) Return(_a0 error) *PeerWrapper_Ready_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PeerWrapper_Ready_Call) RunAndReturn(run func() error) *PeerWrapper_Ready_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: _a0 +func (_m *PeerWrapper) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// PeerWrapper_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type PeerWrapper_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +// - _a0 context.Context +func (_e *PeerWrapper_Expecter) Start(_a0 interface{}) *PeerWrapper_Start_Call { + return &PeerWrapper_Start_Call{Call: _e.mock.On("Start", _a0)} +} + +func (_c *PeerWrapper_Start_Call) Run(run func(_a0 context.Context)) *PeerWrapper_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *PeerWrapper_Start_Call) Return(_a0 error) *PeerWrapper_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PeerWrapper_Start_Call) RunAndReturn(run func(context.Context) error) *PeerWrapper_Start_Call { + _c.Call.Return(run) + return _c +} + +// NewPeerWrapper creates a new instance of PeerWrapper. 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 NewPeerWrapper(t interface { + mock.TestingT + Cleanup(func()) +}) *PeerWrapper { + mock := &PeerWrapper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/libs/x/rage/mocks/shared_peer.go b/libs/x/rage/mocks/shared_peer.go new file mode 100644 index 000000000..7b94c69a1 --- /dev/null +++ b/libs/x/rage/mocks/shared_peer.go @@ -0,0 +1,547 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + mock "github.com/stretchr/testify/mock" + + rage "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +// SharedPeer is an autogenerated mock type for the SharedPeer type +type SharedPeer struct { + mock.Mock +} + +type SharedPeer_Expecter struct { + mock *mock.Mock +} + +func (_m *SharedPeer) EXPECT() *SharedPeer_Expecter { + return &SharedPeer_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with no fields +func (_m *SharedPeer) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SharedPeer_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type SharedPeer_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *SharedPeer_Expecter) Close() *SharedPeer_Close_Call { + return &SharedPeer_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *SharedPeer_Close_Call) Run(run func()) *SharedPeer_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SharedPeer_Close_Call) Return(_a0 error) *SharedPeer_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_Close_Call) RunAndReturn(run func() error) *SharedPeer_Close_Call { + _c.Call.Return(run) + return _c +} + +// HealthReport provides a mock function with no fields +func (_m *SharedPeer) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// SharedPeer_HealthReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HealthReport' +type SharedPeer_HealthReport_Call struct { + *mock.Call +} + +// HealthReport is a helper method to define mock.On call +func (_e *SharedPeer_Expecter) HealthReport() *SharedPeer_HealthReport_Call { + return &SharedPeer_HealthReport_Call{Call: _e.mock.On("HealthReport")} +} + +func (_c *SharedPeer_HealthReport_Call) Run(run func()) *SharedPeer_HealthReport_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SharedPeer_HealthReport_Call) Return(_a0 map[string]error) *SharedPeer_HealthReport_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_HealthReport_Call) RunAndReturn(run func() map[string]error) *SharedPeer_HealthReport_Call { + _c.Call.Return(run) + return _c +} + +// ID provides a mock function with no fields +func (_m *SharedPeer) ID() ragetypes.PeerID { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ID") + } + + var r0 ragetypes.PeerID + if rf, ok := ret.Get(0).(func() ragetypes.PeerID); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ragetypes.PeerID) + } + } + + return r0 +} + +// SharedPeer_ID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ID' +type SharedPeer_ID_Call struct { + *mock.Call +} + +// ID is a helper method to define mock.On call +func (_e *SharedPeer_Expecter) ID() *SharedPeer_ID_Call { + return &SharedPeer_ID_Call{Call: _e.mock.On("ID")} +} + +func (_c *SharedPeer_ID_Call) Run(run func()) *SharedPeer_ID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SharedPeer_ID_Call) Return(_a0 ragetypes.PeerID) *SharedPeer_ID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_ID_Call) RunAndReturn(run func() ragetypes.PeerID) *SharedPeer_ID_Call { + _c.Call.Return(run) + return _c +} + +// IsBootstrap provides a mock function with no fields +func (_m *SharedPeer) IsBootstrap() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsBootstrap") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// SharedPeer_IsBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsBootstrap' +type SharedPeer_IsBootstrap_Call struct { + *mock.Call +} + +// IsBootstrap is a helper method to define mock.On call +func (_e *SharedPeer_Expecter) IsBootstrap() *SharedPeer_IsBootstrap_Call { + return &SharedPeer_IsBootstrap_Call{Call: _e.mock.On("IsBootstrap")} +} + +func (_c *SharedPeer_IsBootstrap_Call) Run(run func()) *SharedPeer_IsBootstrap_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SharedPeer_IsBootstrap_Call) Return(_a0 bool) *SharedPeer_IsBootstrap_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_IsBootstrap_Call) RunAndReturn(run func() bool) *SharedPeer_IsBootstrap_Call { + _c.Call.Return(run) + return _c +} + +// Name provides a mock function with no fields +func (_m *SharedPeer) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// SharedPeer_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type SharedPeer_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *SharedPeer_Expecter) Name() *SharedPeer_Name_Call { + return &SharedPeer_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *SharedPeer_Name_Call) Run(run func()) *SharedPeer_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SharedPeer_Name_Call) Return(_a0 string) *SharedPeer_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_Name_Call) RunAndReturn(run func() string) *SharedPeer_Name_Call { + _c.Call.Return(run) + return _c +} + +// Ready provides a mock function with no fields +func (_m *SharedPeer) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SharedPeer_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' +type SharedPeer_Ready_Call struct { + *mock.Call +} + +// Ready is a helper method to define mock.On call +func (_e *SharedPeer_Expecter) Ready() *SharedPeer_Ready_Call { + return &SharedPeer_Ready_Call{Call: _e.mock.On("Ready")} +} + +func (_c *SharedPeer_Ready_Call) Run(run func()) *SharedPeer_Ready_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SharedPeer_Ready_Call) Return(_a0 error) *SharedPeer_Ready_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_Ready_Call) RunAndReturn(run func() error) *SharedPeer_Ready_Call { + _c.Call.Return(run) + return _c +} + +// Receive provides a mock function with no fields +func (_m *SharedPeer) Receive() <-chan rage.Message { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Receive") + } + + var r0 <-chan rage.Message + if rf, ok := ret.Get(0).(func() <-chan rage.Message); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan rage.Message) + } + } + + return r0 +} + +// SharedPeer_Receive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Receive' +type SharedPeer_Receive_Call struct { + *mock.Call +} + +// Receive is a helper method to define mock.On call +func (_e *SharedPeer_Expecter) Receive() *SharedPeer_Receive_Call { + return &SharedPeer_Receive_Call{Call: _e.mock.On("Receive")} +} + +func (_c *SharedPeer_Receive_Call) Run(run func()) *SharedPeer_Receive_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SharedPeer_Receive_Call) Return(_a0 <-chan rage.Message) *SharedPeer_Receive_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_Receive_Call) RunAndReturn(run func() <-chan rage.Message) *SharedPeer_Receive_Call { + _c.Call.Return(run) + return _c +} + +// Send provides a mock function with given fields: peerID, msg +func (_m *SharedPeer) Send(peerID ragetypes.PeerID, msg []byte) error { + ret := _m.Called(peerID, msg) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if rf, ok := ret.Get(0).(func(ragetypes.PeerID, []byte) error); ok { + r0 = rf(peerID, msg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SharedPeer_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' +type SharedPeer_Send_Call struct { + *mock.Call +} + +// Send is a helper method to define mock.On call +// - peerID ragetypes.PeerID +// - msg []byte +func (_e *SharedPeer_Expecter) Send(peerID interface{}, msg interface{}) *SharedPeer_Send_Call { + return &SharedPeer_Send_Call{Call: _e.mock.On("Send", peerID, msg)} +} + +func (_c *SharedPeer_Send_Call) Run(run func(peerID ragetypes.PeerID, msg []byte)) *SharedPeer_Send_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ragetypes.PeerID), args[1].([]byte)) + }) + return _c +} + +func (_c *SharedPeer_Send_Call) Return(_a0 error) *SharedPeer_Send_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_Send_Call) RunAndReturn(run func(ragetypes.PeerID, []byte) error) *SharedPeer_Send_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: _a0 +func (_m *SharedPeer) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SharedPeer_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type SharedPeer_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +// - _a0 context.Context +func (_e *SharedPeer_Expecter) Start(_a0 interface{}) *SharedPeer_Start_Call { + return &SharedPeer_Start_Call{Call: _e.mock.On("Start", _a0)} +} + +func (_c *SharedPeer_Start_Call) Run(run func(_a0 context.Context)) *SharedPeer_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *SharedPeer_Start_Call) Return(_a0 error) *SharedPeer_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_Start_Call) RunAndReturn(run func(context.Context) error) *SharedPeer_Start_Call { + _c.Call.Return(run) + return _c +} + +// UpdateConnections provides a mock function with given fields: peers +func (_m *SharedPeer) UpdateConnections(peers map[ragetypes.PeerID]rage.StreamConfig) error { + ret := _m.Called(peers) + + if len(ret) == 0 { + panic("no return value specified for UpdateConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[ragetypes.PeerID]rage.StreamConfig) error); ok { + r0 = rf(peers) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SharedPeer_UpdateConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateConnections' +type SharedPeer_UpdateConnections_Call struct { + *mock.Call +} + +// UpdateConnections is a helper method to define mock.On call +// - peers map[ragetypes.PeerID]rage.StreamConfig +func (_e *SharedPeer_Expecter) UpdateConnections(peers interface{}) *SharedPeer_UpdateConnections_Call { + return &SharedPeer_UpdateConnections_Call{Call: _e.mock.On("UpdateConnections", peers)} +} + +func (_c *SharedPeer_UpdateConnections_Call) Run(run func(peers map[ragetypes.PeerID]rage.StreamConfig)) *SharedPeer_UpdateConnections_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[ragetypes.PeerID]rage.StreamConfig)) + }) + return _c +} + +func (_c *SharedPeer_UpdateConnections_Call) Return(_a0 error) *SharedPeer_UpdateConnections_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_UpdateConnections_Call) RunAndReturn(run func(map[ragetypes.PeerID]rage.StreamConfig) error) *SharedPeer_UpdateConnections_Call { + _c.Call.Return(run) + return _c +} + +// UpdateConnectionsByDONs provides a mock function with given fields: ctx, donPairs, streamConfig +func (_m *SharedPeer) UpdateConnectionsByDONs(ctx context.Context, donPairs []rage.DonPair, streamConfig rage.StreamConfig) error { + ret := _m.Called(ctx, donPairs, streamConfig) + + if len(ret) == 0 { + panic("no return value specified for UpdateConnectionsByDONs") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []rage.DonPair, rage.StreamConfig) error); ok { + r0 = rf(ctx, donPairs, streamConfig) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SharedPeer_UpdateConnectionsByDONs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateConnectionsByDONs' +type SharedPeer_UpdateConnectionsByDONs_Call struct { + *mock.Call +} + +// UpdateConnectionsByDONs is a helper method to define mock.On call +// - ctx context.Context +// - donPairs []rage.DonPair +// - streamConfig rage.StreamConfig +func (_e *SharedPeer_Expecter) UpdateConnectionsByDONs(ctx interface{}, donPairs interface{}, streamConfig interface{}) *SharedPeer_UpdateConnectionsByDONs_Call { + return &SharedPeer_UpdateConnectionsByDONs_Call{Call: _e.mock.On("UpdateConnectionsByDONs", ctx, donPairs, streamConfig)} +} + +func (_c *SharedPeer_UpdateConnectionsByDONs_Call) Run(run func(ctx context.Context, donPairs []rage.DonPair, streamConfig rage.StreamConfig)) *SharedPeer_UpdateConnectionsByDONs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]rage.DonPair), args[2].(rage.StreamConfig)) + }) + return _c +} + +func (_c *SharedPeer_UpdateConnectionsByDONs_Call) Return(_a0 error) *SharedPeer_UpdateConnectionsByDONs_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SharedPeer_UpdateConnectionsByDONs_Call) RunAndReturn(run func(context.Context, []rage.DonPair, rage.StreamConfig) error) *SharedPeer_UpdateConnectionsByDONs_Call { + _c.Call.Return(run) + return _c +} + +// NewSharedPeer creates a new instance of SharedPeer. 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 NewSharedPeer(t interface { + mock.TestingT + Cleanup(func()) +}) *SharedPeer { + mock := &SharedPeer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/libs/x/rage/mocks/signer.go b/libs/x/rage/mocks/signer.go new file mode 100644 index 000000000..4d6e8aa59 --- /dev/null +++ b/libs/x/rage/mocks/signer.go @@ -0,0 +1,135 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// Signer is an autogenerated mock type for the Signer type +type Signer struct { + mock.Mock +} + +type Signer_Expecter struct { + mock *mock.Mock +} + +func (_m *Signer) EXPECT() *Signer_Expecter { + return &Signer_Expecter{mock: &_m.Mock} +} + +// Initialize provides a mock function with no fields +func (_m *Signer) Initialize() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Initialize") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Signer_Initialize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Initialize' +type Signer_Initialize_Call struct { + *mock.Call +} + +// Initialize is a helper method to define mock.On call +func (_e *Signer_Expecter) Initialize() *Signer_Initialize_Call { + return &Signer_Initialize_Call{Call: _e.mock.On("Initialize")} +} + +func (_c *Signer_Initialize_Call) Run(run func()) *Signer_Initialize_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Signer_Initialize_Call) Return(_a0 error) *Signer_Initialize_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Signer_Initialize_Call) RunAndReturn(run func() error) *Signer_Initialize_Call { + _c.Call.Return(run) + return _c +} + +// Sign provides a mock function with given fields: data +func (_m *Signer) Sign(data []byte) ([]byte, error) { + ret := _m.Called(data) + + if len(ret) == 0 { + panic("no return value specified for Sign") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func([]byte) ([]byte, error)); ok { + return rf(data) + } + if rf, ok := ret.Get(0).(func([]byte) []byte); ok { + r0 = rf(data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Signer_Sign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sign' +type Signer_Sign_Call struct { + *mock.Call +} + +// Sign is a helper method to define mock.On call +// - data []byte +func (_e *Signer_Expecter) Sign(data interface{}) *Signer_Sign_Call { + return &Signer_Sign_Call{Call: _e.mock.On("Sign", data)} +} + +func (_c *Signer_Sign_Call) Run(run func(data []byte)) *Signer_Sign_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *Signer_Sign_Call) Return(_a0 []byte, _a1 error) *Signer_Sign_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Signer_Sign_Call) RunAndReturn(run func([]byte) ([]byte, error)) *Signer_Sign_Call { + _c.Call.Return(run) + return _c +} + +// NewSigner creates a new instance of Signer. 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 NewSigner(t interface { + mock.TestingT + Cleanup(func()) +}) *Signer { + mock := &Signer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/libs/x/rage/peer.go b/libs/x/rage/peer.go new file mode 100644 index 000000000..3fb7469a0 --- /dev/null +++ b/libs/x/rage/peer.go @@ -0,0 +1,244 @@ +package rage + +import ( + "context" + "crypto" + "crypto/ed25519" + "fmt" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/smartcontractkit/libocr/networking/ragedisco" + nettypes "github.com/smartcontractkit/libocr/networking/types" + "github.com/smartcontractkit/libocr/ragep2p" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +var ( + defaultStreamName = "stream" + defaultRecvChSize = 10000 +) + +type PeerConfig struct { + PrivateKey crypto.Signer + // List of : addresses. + ListenAddresses []string + // List of : addresses. If empty, defaults to ListenAddresses. + AnnounceAddresses []string + Bootstrappers []ragetypes.PeerInfo + // Every DeltaReconcile a Reconcile message is sent to every peer. + DeltaReconcile time.Duration + // Dial attempts will be at least DeltaDial apart. + DeltaDial time.Duration + DiscovererDatabase nettypes.DiscovererDatabase + MetricsRegisterer prometheus.Registerer +} + +type peer struct { + streams map[ragetypes.PeerID]*ragep2p.Stream + cfg PeerConfig + isBootstrap bool + host *ragep2p.Host + discoverer *ragedisco.Ragep2pDiscoverer + myID ragetypes.PeerID + recvCh chan Message + + stopCh services.StopChan + wg sync.WaitGroup + lggr logger.Logger + groupID *counter +} + +var _ Peer = &peer{} + +func NewPeer(cfg PeerConfig, lggr logger.Logger) (*peer, error) { + peerID, err := ragetypes.PeerIDFromPublicKey(cfg.PrivateKey.Public().(ed25519.PublicKey)) + if err != nil { + return nil, fmt.Errorf("error extracting v2 peer ID from private key: %w", err) + } + isBootstrap := false + for _, info := range cfg.Bootstrappers { + if info.ID == peerID { + isBootstrap = true + break + } + } + + announceAddresses := cfg.AnnounceAddresses + if len(cfg.AnnounceAddresses) == 0 { + announceAddresses = cfg.ListenAddresses + } + + discoverer := ragedisco.NewRagep2pDiscoverer(cfg.DeltaReconcile, announceAddresses, cfg.DiscovererDatabase, cfg.MetricsRegisterer) + commonLggr := logger.NewOCRWrapper(lggr, true, func(string) {}) + + peerKeyring, err := NewSignerPeerKeyring(cfg.PrivateKey) + if err != nil { + return nil, err + } + + host, err := ragep2p.NewHost( + ragep2p.HostConfig{DurationBetweenDials: cfg.DeltaDial}, + peerKeyring, + cfg.ListenAddresses, + discoverer, + commonLggr, + cfg.MetricsRegisterer, + ) + if err != nil { + return nil, fmt.Errorf("failed to construct ragep2p host: %w", err) + } + + return &peer{ + streams: make(map[ragetypes.PeerID]*ragep2p.Stream), + cfg: cfg, + isBootstrap: isBootstrap, + host: host, + discoverer: discoverer, + myID: peerID, + recvCh: make(chan Message, defaultRecvChSize), + stopCh: make(services.StopChan), + lggr: logger.Named(lggr, "P2PPeer"), + groupID: &counter{}, + }, nil +} + +func (p *peer) ID() ragetypes.PeerID { + return p.myID +} + +func (p *peer) UpdateConnections(peers map[ragetypes.PeerID]StreamConfig) error { + p.lggr.Infow("updating peer addresses", "peers", peers) + if !p.isBootstrap { + if err := p.recreateStreams(peers); err != nil { + return err + } + } + // updating the group is a small optimization that avoids reconnecting to existing peers + currentGroupID := p.groupID.Bytes() + newGroupID := p.groupID.Inc().Bytes() + peerIDs := []ragetypes.PeerID{} + for pid := range peers { + peerIDs = append(peerIDs, pid) + } + if err := p.discoverer.AddGroup(newGroupID, peerIDs, p.cfg.Bootstrappers); err != nil { + p.lggr.Warnw("failed to add group", "groupID", newGroupID) + return err + } + if err := p.discoverer.RemoveGroup(currentGroupID); err != nil { + p.lggr.Warnw("failed to remove old group", "groupID", currentGroupID) + } + + return nil +} + +func (p *peer) recreateStreams(peers map[ragetypes.PeerID]StreamConfig) error { + for pid, cfg := range peers { + if pid == p.myID { // don't create a self-stream + continue + } + _, ok := p.streams[pid] + if ok { // already have a stream with this peer + continue + } + + stream, err := p.host.NewStream( + pid, + defaultStreamName, + cfg.OutgoingMessageBufferSize, + cfg.IncomingMessageBufferSize, + cfg.MaxMessageLenBytes, + cfg.MessageRateLimiter, + cfg.BytesRateLimiter, + ) + if err != nil { + return fmt.Errorf("failed to create stream to peer id: %q: %w", pid, err) + } + p.lggr.Infow("adding peer", "peerID", pid) + p.streams[pid] = stream + p.wg.Add(1) + go p.recvLoopSingle(pid, stream.ReceiveMessages()) + } + // remove obsolete streams + for pid, stream := range p.streams { + _, ok := peers[pid] + if !ok { + p.lggr.Infow("removing peer", "peerID", pid) + delete(p.streams, pid) + err := stream.Close() + if err != nil { + p.lggr.Errorw("failed to close stream", "peerID", pid, "error", err) + } + } + } + return nil +} + +func (p *peer) Start(ctx context.Context) error { + err := p.host.Start() + if err != nil { + return fmt.Errorf("failed to start ragep2p host: %w", err) + } + p.lggr.Info("peer started") + return nil +} + +func (p *peer) recvLoopSingle(pid ragetypes.PeerID, ch <-chan []byte) { + p.lggr.Infow("starting recvLoopSingle", "peerID", pid) + defer p.wg.Done() + for { + select { + case <-p.stopCh: + p.lggr.Infow("stopped - exiting recvLoopSingle", "peerID", pid) + return + case msg, ok := <-ch: + if !ok { + p.lggr.Infow("channel closed - exiting recvLoopSingle", "peerID", pid) + return + } + p.recvCh <- Message{Sender: pid, Payload: msg} + } + } +} + +func (p *peer) Send(peerID ragetypes.PeerID, msg []byte) error { + stream, ok := p.streams[peerID] + if !ok { + return fmt.Errorf("no stream to peer id: %q", peerID) + } + stream.SendMessage(msg) + return nil +} + +func (p *peer) Receive() <-chan Message { + return p.recvCh +} + +func (p *peer) Close() error { + err := p.host.Close() + close(p.stopCh) + p.wg.Wait() + p.lggr.Info("peer closed") + return err +} + +func (p *peer) Ready() error { + return nil +} + +func (p *peer) HealthReport() map[string]error { + return nil +} + +func (p *peer) Name() string { + return p.lggr.Name() +} + +func (p *peer) IsBootstrap() bool { + return p.isBootstrap +} diff --git a/libs/x/rage/peer_test.go b/libs/x/rage/peer_test.go new file mode 100644 index 000000000..d83fb7c71 --- /dev/null +++ b/libs/x/rage/peer_test.go @@ -0,0 +1,51 @@ +package rage_test + +import ( + "crypto/ed25519" + "crypto/rand" + "fmt" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/freeport" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +func TestPeer_CleanStartClose(t *testing.T) { + lggr := logger.Test(t) + port := freeport.GetOne(t) + privKey, _ := newKeyPair(t) + + reg := prometheus.NewRegistry() + peerConfig := rage.PeerConfig{ + PrivateKey: privKey, + ListenAddresses: []string{fmt.Sprintf("127.0.0.1:%d", port)}, + + DeltaReconcile: time.Second * 5, + DeltaDial: time.Second * 5, + DiscovererDatabase: rage.NewInMemoryDiscovererDatabase(), + MetricsRegisterer: reg, + } + + peer, err := rage.NewPeer(peerConfig, lggr) + require.NoError(t, err) + err = peer.Start(t.Context()) + require.NoError(t, err) + err = peer.Close() + require.NoError(t, err) +} + +func newKeyPair(t *testing.T) (ed25519.PrivateKey, ragetypes.PeerID) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + peerID, err := ragetypes.PeerIDFromPrivateKey(privKey) + require.NoError(t, err) + return privKey, peerID +} diff --git a/libs/x/rage/shared_peer.go b/libs/x/rage/shared_peer.go new file mode 100644 index 000000000..beeff87b7 --- /dev/null +++ b/libs/x/rage/shared_peer.go @@ -0,0 +1,402 @@ +package rage + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "slices" + "strings" + "sync" + "time" + + ocrcommontypes "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/networking" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// Don2DonSharedPeer is a wrapper around PeerGroupFactory created by SingletonPeerWrapper. +// It manages peer groups and their streams for Don2Don traffic. +// +// Don2DonSharedPeer creates two types of PeerGroups: +// - Discovery Groups - each consisting of all peers from exactly two DONs. +// Those groups are used for peer discovery and won’t have any Streams created within them. +// - Messaging Groups (“pairs”) - each consisting of exactly two peers - the current peer +// and one remote peer. Each of those groups will always have exactly one Stream +// created within them, which will be used to exchange Don2Don messages. +// +// Thread-safety: after calling Start(), Send() can be called concurrently from multiple goroutines, +// UpdateConnectionsByDONs() can be called concurrently with Send() but only from a single goroutine. +type don2DonSharedPeer struct { + services.Service + srvcEng *services.Engine + peerSource PeerSource + bootstrappers []ocrcommontypes.BootstrapperLocator + lggr logger.Logger + + // fields derived from config and dependencies + pgFactory networking.PeerGroupFactory + myID ragetypes.PeerID + isBootstrap bool + + recvCh chan Message + discoveryGroups map[string]networking.PeerGroup // keyed by donPairHash() + remotePeers map[ragetypes.PeerID]*remotePeer + mu sync.RWMutex // protects discoveryGroups and remotePeers + + metrics *SharedPeerMetrics +} + +var _ SharedPeer = &don2DonSharedPeer{} + +type remotePeer struct { + // A PeerGroup with exactly two members, connecting our peer with a single remote peer. + peerPairGroup networking.PeerGroup + // Stream managed by the PeerGroup. + stream networking.Stream +} + +// PeerSource supplies the started peer this rides on: its group factory and its identity. Both are +// read when this service starts rather than when it is built, because the peer is usually started +// after it - which is also why this is an interface rather than the two values. +type PeerSource interface { + PeerGroupFactory() networking.PeerGroupFactory + PeerID() ragetypes.PeerID +} + +func NewDon2DonSharedPeer(peerSource PeerSource, bootstrappers []ocrcommontypes.BootstrapperLocator, lggr logger.Logger) *don2DonSharedPeer { + sp := &don2DonSharedPeer{ + peerSource: peerSource, + bootstrappers: bootstrappers, + recvCh: make(chan Message, defaultRecvChSize), + discoveryGroups: make(map[string]networking.PeerGroup), + remotePeers: make(map[ragetypes.PeerID]*remotePeer), + lggr: logger.Named(lggr, "Don2DonSharedPeer"), + } + sp.Service, sp.srvcEng = services.Config{ + Name: "Don2DonSharedPeer", + Start: sp.start, + Close: sp.close, + }.NewServiceEngine(sp.lggr) + return sp +} + +func (sp *don2DonSharedPeer) start(ctx context.Context) error { + sp.lggr.Info("Starting Don2DonSharedPeer ...") + if sp.peerSource == nil { + return errors.New("field PeerSource is not set") + } + sp.pgFactory = sp.peerSource.PeerGroupFactory() + if sp.pgFactory == nil { + return errors.New("the peer source has no PeerGroupFactory; it was probably not started before Don2DonSharedPeer, or failed to initialize") + } + sp.myID = sp.peerSource.PeerID() + if (sp.myID == ragetypes.PeerID{}) { + return errors.New("the peer source has no PeerID; was it started before Don2DonSharedPeer?") + } + myIDStr := sp.myID.String() + for _, bootstrapper := range sp.bootstrappers { + if bootstrapper.PeerID == myIDStr { + sp.isBootstrap = true + break + } + } + metrics, err := initSharedPeerMetrics() + if err != nil { + return fmt.Errorf("failed to init Don2DonSharedPeer metrics: %w", err) + } + sp.metrics = metrics + sp.lggr.Infow("Started Don2DonSharedPeer", "peerId", myIDStr, "isBootstrap", sp.isBootstrap) + return nil +} + +func (sp *don2DonSharedPeer) close() error { + sp.mu.Lock() + defer sp.mu.Unlock() + sp.lggr.Info("Closing Don2DonSharedPeer ...") + for name, group := range sp.discoveryGroups { + if group.Close() != nil { + sp.lggr.Errorw("failed to close discovery group", "name", name) + } + } + sp.discoveryGroups = make(map[string]networking.PeerGroup) + for pid, peer := range sp.remotePeers { + if peer.peerPairGroup.Close() != nil { + sp.lggr.Errorw("failed to close messaging group peer", "remotePeerID", pid.String()) + } + } + sp.remotePeers = make(map[ragetypes.PeerID]*remotePeer) + close(sp.recvCh) // all writer goroutines are stopped by now so it's safe to close + sp.lggr.Info("Closed Don2DonSharedPeer") + return nil +} + +func (sp *don2DonSharedPeer) ID() ragetypes.PeerID { + return sp.myID +} + +func (sp *don2DonSharedPeer) IsBootstrap() bool { + return sp.isBootstrap +} + +func (sp *don2DonSharedPeer) Send(peerID ragetypes.PeerID, msg []byte) error { + sp.mu.RLock() + rp, ok := sp.remotePeers[peerID] + sp.mu.RUnlock() + if !ok { + return fmt.Errorf("no stream to remote peer id: %s, from local peer id: %s", peerID.String(), sp.myID.String()) + } + rp.stream.SendMessage(msg) + return nil +} + +func (sp *don2DonSharedPeer) Receive() <-chan Message { + return sp.recvCh +} + +func (sp *don2DonSharedPeer) UpdateConnections(peers map[ragetypes.PeerID]StreamConfig) error { + return errors.New("UpdateConnections is not supported, use UpdateConnectionsByDON instead") +} + +func (sp *don2DonSharedPeer) UpdateConnectionsByDONs(ctx context.Context, donPairs []DonPair, streamConfig StreamConfig) error { + sp.lggr.Infow("UpdateConnectionsByDONs", "numDonPairs", len(donPairs)) + startTs := time.Now().UnixMilli() + + desiredDONPairsIDs := make(map[string]struct{}) + for _, dp := range donPairs { + pairID := pairID(dp[0], dp[1]) + desiredDONPairsIDs[pairID] = struct{}{} + } + desiredRemotePeers := make(map[ragetypes.PeerID]struct{}) + for _, dp := range donPairs { + if slices.Contains(dp[0].Members, sp.myID) { + for _, pid := range dp[1].Members { + desiredRemotePeers[pid] = struct{}{} + } + } + if slices.Contains(dp[1].Members, sp.myID) { + for _, pid := range dp[0].Members { + desiredRemotePeers[pid] = struct{}{} + } + } + } + delete(desiredRemotePeers, sp.myID) // don't accidentally create a stream to ourselves + + // Most of the time, there are no changes to peer groups and we can short-circuit. + // If changes are needed, let's perform updates under a write lock. + // A large change is expected only once, on startup, when all groups are created. + if !sp.stateEqual(desiredDONPairsIDs, desiredRemotePeers) { + err := sp.updateConnections(donPairs, desiredDONPairsIDs, desiredRemotePeers, streamConfig) + if err != nil { + sp.metrics.groupUpdateFailureCounter.Add(ctx, 1) + return fmt.Errorf("failed to update connections by DONs: %w", err) + } + } + + sp.mu.RLock() + defer sp.mu.RUnlock() + sp.metrics.discoveryGroups.Record(ctx, int64(len(sp.discoveryGroups))) + sp.metrics.messagingGroups.Record(ctx, int64(len(sp.remotePeers))) + sp.metrics.groupUpdateDurationMs.Record(ctx, time.Now().UnixMilli()-startTs) + sp.lggr.Info("UpdateConnectionsByDONs done") + return nil +} + +// Return true if current state is equal to desider state. +func (sp *don2DonSharedPeer) stateEqual(desiredDONPairsIDs map[string]struct{}, desiredRemotePeers map[ragetypes.PeerID]struct{}) bool { + sp.mu.RLock() + defer sp.mu.RUnlock() + if len(sp.discoveryGroups) != len(desiredDONPairsIDs) || len(sp.remotePeers) != len(desiredRemotePeers) { + return false + } + for donPairID := range desiredDONPairsIDs { + if _, ok := sp.discoveryGroups[donPairID]; !ok { + return false + } + } + for pid := range desiredRemotePeers { + if _, ok := sp.remotePeers[pid]; !ok { + return false + } + } + return true +} + +func (sp *don2DonSharedPeer) updateConnections(donPairs []DonPair, desiredDONPairsIDs map[string]struct{}, desiredRemotePeers map[ragetypes.PeerID]struct{}, streamConfig StreamConfig) error { + sp.mu.Lock() + defer sp.mu.Unlock() + // Phase 1 - create discovery groups. Those groups consist of peers from exactly two DONs. + for _, dp := range donPairs { + pairID := pairID(dp[0], dp[1]) + if _, ok := sp.discoveryGroups[pairID]; ok { + continue // group already exists, no need to recreate it + } + peerSet := make(map[string]struct{}) + for _, pid := range dp[0].Members { + peerSet[pid.String()] = struct{}{} + } + for _, pid := range dp[1].Members { + peerSet[pid.String()] = struct{}{} + } + peers := make([]string, 0, len(peerSet)) + for pidStr := range peerSet { + peers = append(peers, pidStr) // no duplicate peers + } + + if _, ok := sp.discoveryGroups[pairID]; !ok { + digest := donPairDigest(dp[0].ID, dp[1].ID) + peerGroup, err := sp.pgFactory.NewPeerGroup(digest, peers, sp.bootstrappers) + if err != nil { + sp.lggr.Errorw("failed to create discovery group", "digest", digest, "err", err) + return fmt.Errorf("failed to create discovery group: %w", err) + } + sp.lggr.Infow("Created discovery group", "donPairId", pairID, "don1", dp[0].ID, "don2", dp[1].ID, "numPeers", len(peers)) + sp.discoveryGroups[pairID] = peerGroup + } + } + + // Remove obsolete groups + for donPairID := range sp.discoveryGroups { + if _, ok := desiredDONPairsIDs[donPairID]; !ok { + peerGroup := sp.discoveryGroups[donPairID] + err := peerGroup.Close() + if err != nil { + sp.lggr.Errorw("failed to close discovery group", "donPairId", donPairID, "err", err) + } + delete(sp.discoveryGroups, donPairID) + sp.lggr.Infow("Closed discovery group", "donPairId", donPairID) + } + } + + // Phase 2 - create messaging groups for Don2Don streams. Each group consists of exactly two Peers. + if !sp.isBootstrap { + for remotePID := range desiredRemotePeers { + if _, exists := sp.remotePeers[remotePID]; exists { + continue // stream to this peer already exists + } + // Create a group with only our peer and the remote peer + peers := []string{sp.myID.String(), remotePID.String()} + digest := nodePairDigest(sp.myID, remotePID) + peerGroup, err := sp.pgFactory.NewPeerGroup(digest, peers, sp.bootstrappers) + if err != nil { + sp.lggr.Errorw("failed to create remote peer group", "digest", digest, "err", err) + return fmt.Errorf("failed to create remote peer group: %w", err) + } + cfg := networking.NewStreamArgs1{ + StreamName: streamName(digest, sp.myID, remotePID), + OutgoingBufferSize: streamConfig.OutgoingMessageBufferSize, + IncomingBufferSize: streamConfig.IncomingMessageBufferSize, + MaxMessageLength: streamConfig.MaxMessageLenBytes, + MessagesLimit: streamConfig.MessageRateLimiter, + BytesLimit: streamConfig.BytesRateLimiter, + } + stream, err := peerGroup.NewStream(remotePID.String(), cfg) + if err != nil { + sp.lggr.Errorw("failed to create stream for remote peer", "peerID", remotePID, "err", err) + peerGroup.Close() + return fmt.Errorf("failed to create stream for remote peer: %w", err) + } + sp.remotePeers[remotePID] = &remotePeer{ + peerPairGroup: peerGroup, + stream: stream, + } + sp.srvcEng.Go(func(srvcCtx context.Context) { + sp.recvLoopSingle(srvcCtx, remotePID, stream.ReceiveMessages()) + }) + sp.lggr.Infow("Created stream to remote peer", "remotePeerID", remotePID) + } + + // Remove obsolete remote peers + for pid := range sp.remotePeers { + if _, ok := desiredRemotePeers[pid]; !ok { + rp := sp.remotePeers[pid] + if rp != nil && rp.peerPairGroup != nil { + rp.peerPairGroup.Close() // closes the stream + } + delete(sp.remotePeers, pid) + sp.lggr.Infow("Closed stream to remote peer", "remotePeerID", pid) + } + } + } + return nil +} + +func (sp *don2DonSharedPeer) recvLoopSingle(ctx context.Context, pid ragetypes.PeerID, ch <-chan []byte) { + sp.lggr.Infow("starting recvLoopSingle", "peerID", pid) + for { + select { + case <-ctx.Done(): + sp.lggr.Infow("stopped - exiting recvLoopSingle", "peerID", pid) + return + case msg, ok := <-ch: + if !ok { + sp.lggr.Infow("channel closed - exiting recvLoopSingle", "peerID", pid) + return + } + sp.recvCh <- Message{Sender: pid, Payload: msg} + } + } +} + +// Create a unique ID based of both DON IDs and all sorted DON members to uniquely represent a DON pair +func pairID(donA, donB capabilities.DON) string { + if donA.ID > donB.ID { + donA, donB = donB, donA + } + memberIDsA := make([]string, 0, len(donA.Members)) + for _, pid := range donA.Members { + memberIDsA = append(memberIDsA, pid.String()) + } + slices.Sort(memberIDsA) + hashPeersA := sha256.Sum256([]byte(strings.Join(memberIDsA, ","))) + memberIDsB := make([]string, 0, len(donB.Members)) + for _, pid := range donB.Members { + memberIDsB = append(memberIDsB, pid.String()) + } + slices.Sort(memberIDsB) + hashPeersB := sha256.Sum256([]byte(strings.Join(memberIDsB, ","))) + return fmt.Sprintf("%d-%d-%s-%s", donA.ID, donB.ID, hex.EncodeToString(hashPeersA[:]), hex.EncodeToString(hashPeersB[:])) +} + +func donPairDigest(donID1, donID2 uint32) ocr2types.ConfigDigest { + // Create a digest based on sorted DON IDs + if donID1 > donID2 { + donID1, donID2 = donID2, donID1 + } + var digest ocr2types.ConfigDigest + binary.BigEndian.PutUint16(digest[:], uint16(ocr2types.ConfigDigestPrefixDONToDONDiscoveryGroup)) + binary.BigEndian.PutUint32(digest[2:], donID1) + binary.BigEndian.PutUint32(digest[6:], donID2) + return digest +} + +func nodePairDigest(peerID1, peerID2 ragetypes.PeerID) ocr2types.ConfigDigest { + id1Str, id2Str := peerIDStrings(peerID1, peerID2) + var digest ocr2types.ConfigDigest + binary.BigEndian.PutUint16(digest[:], uint16(ocr2types.ConfigDigestPrefixDONToDONMessagingGroup)) + combinedHash := sha256.Sum256(([]byte(id1Str + id2Str))) + copy(digest[2:], combinedHash[:30]) + return digest +} + +func peerIDStrings(peerID1, peerID2 ragetypes.PeerID) (string, string) { + id1Str := peerID1.String() + id2Str := peerID2.String() + if id1Str > id2Str { + id1Str, id2Str = id2Str, id1Str + } + return id1Str, id2Str +} + +func streamName(digest ocr2types.ConfigDigest, peerID1, peerID2 ragetypes.PeerID) string { + id1Str, id2Str := peerIDStrings(peerID1, peerID2) + // NOTE: stream name prefix needs to match https://github.com/smartcontractkit/libocr/blob/master/networking/peer_group.go#L25 + return fmt.Sprintf("don-to-don/%s/%s-%s", digest, id1Str, id2Str) +} diff --git a/libs/x/rage/shared_peer_test.go b/libs/x/rage/shared_peer_test.go new file mode 100644 index 000000000..7e3600cbe --- /dev/null +++ b/libs/x/rage/shared_peer_test.go @@ -0,0 +1,133 @@ +package rage_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/networking" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +type fakePeerSource struct { + pgFactory networking.PeerGroupFactory + peerID ragetypes.PeerID +} + +func (f *fakePeerSource) PeerGroupFactory() networking.PeerGroupFactory { return f.pgFactory } +func (f *fakePeerSource) PeerID() ragetypes.PeerID { return f.peerID } + +func TestDon2DonSharedPeer_ErrorOnNilPeerSource(t *testing.T) { + sp := rage.NewDon2DonSharedPeer(nil, nil, logger.Test(t)) + require.Error(t, sp.Start(t.Context())) +} + +func TestDon2DonSharedPeer_UpdateConnectionsByDONs(t *testing.T) { + _, myPeerID := newKeyPair(t) + _, peerID2 := newKeyPair(t) + _, peerID3 := newKeyPair(t) + _, peerID4 := newKeyPair(t) + mockPGFactory := mockPeerGroupFactory{} + source := &fakePeerSource{pgFactory: &mockPGFactory, peerID: myPeerID} + + sp := rage.NewDon2DonSharedPeer(source, nil, logger.Test(t)) + require.NoError(t, sp.Start(t.Context())) + + donPairs := []rage.DonPair{{ + {ID: 1, Members: []ragetypes.PeerID{myPeerID, peerID2}}, + {ID: 2, Members: []ragetypes.PeerID{peerID2, peerID3}}, + }} + // Adding a new DON pair + require.NoError(t, sp.UpdateConnectionsByDONs(t.Context(), donPairs, rage.StreamConfig{})) + require.Equal(t, 1, mockPGFactory.newDonGroupCounter) + require.Equal(t, 2, mockPGFactory.newNodeGroupCounter) // myPeer is connected to peers 2 and 3 + require.Equal(t, 0, mockPGFactory.closedGroupCounter) + require.Equal(t, 2, mockPGFactory.newStreamCounter) + + // No changes expected when updating the same group + require.NoError(t, sp.UpdateConnectionsByDONs(t.Context(), donPairs, rage.StreamConfig{})) + require.Equal(t, 1, mockPGFactory.newDonGroupCounter) + require.Equal(t, 2, mockPGFactory.newNodeGroupCounter) + require.Equal(t, 0, mockPGFactory.closedGroupCounter) + require.Equal(t, 2, mockPGFactory.newStreamCounter) + + // Expect a change when DON membership changes + donPairs[0][1].Members[1] = peerID4 + require.NoError(t, sp.UpdateConnectionsByDONs(t.Context(), donPairs, rage.StreamConfig{})) + require.Equal(t, 2, mockPGFactory.newDonGroupCounter) // update of existing group + require.Equal(t, 3, mockPGFactory.newNodeGroupCounter) // one new connection to peer 4 + require.Equal(t, 2, mockPGFactory.closedGroupCounter) // close old DON group + peer 2 group + require.Equal(t, 3, mockPGFactory.newStreamCounter) // one new connection to peer 4 + + // Expect a change when a new DON pair is added + donPairs = append(donPairs, [2]capabilities.DON{ + {ID: 3, Members: []ragetypes.PeerID{myPeerID, peerID3}}, + {ID: 2, Members: []ragetypes.PeerID{peerID2, peerID3}}, + }) + require.NoError(t, sp.UpdateConnectionsByDONs(t.Context(), donPairs, rage.StreamConfig{})) + require.Equal(t, 3, mockPGFactory.newDonGroupCounter) + require.Equal(t, 4, mockPGFactory.newNodeGroupCounter) // re-create connection to peer 2 + require.Equal(t, 2, mockPGFactory.closedGroupCounter) + require.Equal(t, 4, mockPGFactory.newStreamCounter) // re-create connection to peer 2 + + require.NoError(t, sp.Close()) + require.Equal(t, 2+2+3, mockPGFactory.closedGroupCounter) // closed 2 DON groups and 3 node groups +} + +type mockPeerGroupFactory struct { + newDonGroupCounter int // large - more than 2 members + newNodeGroupCounter int // small - 2 members + closedGroupCounter int + newStreamCounter int +} + +func (m *mockPeerGroupFactory) NewPeerGroup( + configDigest ocr2types.ConfigDigest, + peerIDs []string, + bootstrappers []commontypes.BootstrapperLocator, +) (networking.PeerGroup, error) { + if len(peerIDs) > 2 { + m.newDonGroupCounter++ + } else { + m.newNodeGroupCounter++ + } + return &mockPeerGroup{groupFactory: m}, nil +} + +type mockPeerGroup struct { + groupFactory *mockPeerGroupFactory +} + +func (m *mockPeerGroup) NewStream(remotePeerID string, newStreamArgs networking.NewStreamArgs) (networking.Stream, error) { + m.groupFactory.newStreamCounter++ + return &mockStream{msgCh: make(chan []byte)}, nil +} + +func (m *mockPeerGroup) Close() error { + m.groupFactory.closedGroupCounter++ + return nil +} + +type mockStream struct { + msgCh chan []byte +} + +func (m *mockStream) SendMessage(data []byte) { + m.msgCh <- data +} + +func (m *mockStream) ReceiveMessages() <-chan []byte { + return m.msgCh +} + +func (m *mockStream) Close() error { + close(m.msgCh) + return nil +} diff --git a/libs/x/rage/types.go b/libs/x/rage/types.go new file mode 100644 index 000000000..19cb2ac44 --- /dev/null +++ b/libs/x/rage/types.go @@ -0,0 +1,65 @@ +// Package rage is the rage p2p transport DON-to-DON traffic rides on: a peer, the shared peer that +// keeps a group and a stream per remote node, and the types they exchange. +// +// It lives under libs/x because it is on its way somewhere else. crecore is meant to own DON-to-DON +// communication outright, at which point this moves into crecore and stops being importable at all; +// it is here only because core still uses it and both need to build against the same code during +// the move. Nothing else should import it - if you need to talk to another DON, ask crecore. +// +// Moved from chainlink's core/services/p2p, with the interfaces and their rage implementation +// collapsed into one package and the two dependencies on core replaced: the keyring adapter is now +// local (see keyring.go) and the shared peer takes a PeerSource instead of core's +// SingletonPeerWrapper. +package rage + +import ( + "context" + + "github.com/smartcontractkit/libocr/ragep2p" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +const PeerIDLength = 32 + +type PeerID = ragetypes.PeerID + +type Peer interface { + services.Service + ID() PeerID + UpdateConnections(peers map[PeerID]StreamConfig) error + Send(peerID PeerID, msg []byte) error + Receive() <-chan Message + IsBootstrap() bool +} + +type DonPair [2]capabilities.DON +type SharedPeer interface { + Peer + UpdateConnectionsByDONs(ctx context.Context, donPairs []DonPair, streamConfig StreamConfig) error +} + +type PeerWrapper interface { + services.Service + GetPeer() Peer +} + +type Signer interface { + Initialize() error + Sign(data []byte) ([]byte, error) +} + +type Message struct { + Sender PeerID + Payload []byte +} + +type StreamConfig struct { + IncomingMessageBufferSize int + OutgoingMessageBufferSize int + MaxMessageLenBytes int + MessageRateLimiter ragep2p.TokenBucketParams + BytesRateLimiter ragep2p.TokenBucketParams +} diff --git a/libs/x/rage/utils.go b/libs/x/rage/utils.go new file mode 100644 index 000000000..d45f6a779 --- /dev/null +++ b/libs/x/rage/utils.go @@ -0,0 +1,32 @@ +package rage + +import ( + "context" + + ocrnetworking "github.com/smartcontractkit/libocr/networking/types" +) + +var _ ocrnetworking.DiscovererDatabase = &InMemoryDiscovererDatabase{} + +type InMemoryDiscovererDatabase struct { + store map[string][]byte +} + +func NewInMemoryDiscovererDatabase() *InMemoryDiscovererDatabase { + return &InMemoryDiscovererDatabase{make(map[string][]byte)} +} + +func (d *InMemoryDiscovererDatabase) StoreAnnouncement(ctx context.Context, peerID string, ann []byte) error { + d.store[peerID] = ann + return nil +} + +func (d *InMemoryDiscovererDatabase) ReadAnnouncements(ctx context.Context, peerIDs []string) (map[string][]byte, error) { + ret := make(map[string][]byte) + for _, peerID := range peerIDs { + if ann, ok := d.store[peerID]; ok { + ret[peerID] = ann + } + } + return ret, nil +} diff --git a/libs/x/rage/utils_test.go b/libs/x/rage/utils_test.go new file mode 100644 index 000000000..01bd49c56 --- /dev/null +++ b/libs/x/rage/utils_test.go @@ -0,0 +1,28 @@ +package rage_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/capabilities/libs/x/rage" +) + +const ( + peerID1 = "peer1" + peerID2 = "peer2" + ann1 = "announcement1" + ann2 = "announcement2" +) + +func TestInMemoryDiscovererDatabase(t *testing.T) { + db := rage.NewInMemoryDiscovererDatabase() + require.NoError(t, db.StoreAnnouncement(t.Context(), peerID1, []byte(ann1))) + require.NoError(t, db.StoreAnnouncement(t.Context(), peerID2, []byte(ann2))) + state, err := db.ReadAnnouncements(t.Context(), []string{peerID1, peerID2}) + require.NoError(t, err) + require.Equal(t, map[string][]byte{peerID1: []byte(ann1), peerID2: []byte(ann2)}, state) + state, err = db.ReadAnnouncements(t.Context(), []string{peerID2}) + require.NoError(t, err) + require.Equal(t, map[string][]byte{peerID2: []byte(ann2)}, state) +} From 6a777f5393fa405ccfdea6ef601183d8bd0b8d45 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 13 Aug 2026 12:56:01 -0400 Subject: [PATCH 16/30] Cleaner and more hardened registry refactors core's --- crecore/endpoint2_server.go | 17 +- crecore/go.mod | 4 +- crecore/main.go | 20 +- .../0002_create_proxy_registry_snapshots.sql | 14 + crecore/registry/local_registry.go | 232 +----- crecore/registry/metadata.go | 62 -- crecore/registry/registry.go | 326 ++++++++ crecore/registry/server.go | 213 ++++++ crecore/registry/server_test.go | 707 ++++++++++++++++++ crecore/registry/syncer.go | 115 ++- crecore/registry/syncer_test.go | 151 +++- crecore/registry_service.go | 118 +-- crecore/server.go | 24 +- libs/standalone/bootstrapper.go | 3 + libs/standalone/cmd_helpers.go | 27 - libs/standalone/config.go | 4 +- libs/x/README.md | 14 + libs/x/registrysyncer/local_registry.go | 267 +++++++ .../x/registrysyncer}/local_registry_test.go | 9 +- libs/x/registrysyncer/orm.go | 194 +++++ libs/x/registrysyncer/orm_test.go | 85 +++ 21 files changed, 2153 insertions(+), 453 deletions(-) create mode 100644 crecore/migrations/0002_create_proxy_registry_snapshots.sql delete mode 100644 crecore/registry/metadata.go create mode 100644 crecore/registry/registry.go create mode 100644 crecore/registry/server.go create mode 100644 crecore/registry/server_test.go delete mode 100644 libs/standalone/cmd_helpers.go create mode 100644 libs/x/registrysyncer/local_registry.go rename {crecore/registry => libs/x/registrysyncer}/local_registry_test.go (96%) create mode 100644 libs/x/registrysyncer/orm.go create mode 100644 libs/x/registrysyncer/orm_test.go diff --git a/crecore/endpoint2_server.go b/crecore/endpoint2_server.go index 6fa04b888..e32719b8e 100644 --- a/crecore/endpoint2_server.go +++ b/crecore/endpoint2_server.go @@ -1,8 +1,10 @@ package main import ( + "errors" "fmt" "io" + "math" "sync" "time" @@ -78,7 +80,7 @@ func (s *Endpoint2Server) Connect(stream creproxy.Endpoint2Proxy_ConnectServer) for { req, err := stream.Recv() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil } return err @@ -88,9 +90,15 @@ func (s *Endpoint2Server) Connect(stream creproxy.Endpoint2Proxy_ConnectServer) case *creproxy.Endpoint2ClientRequest_NewEndpoint: return fmt.Errorf("NewEndpoint2Request not allowed after initial setup") case *creproxy.Endpoint2ClientRequest_SendTo: + // See toOracleID: an out-of-range ID is refused rather than truncated into some + // other oracle's. + to, ok := toOracleID(m.SendTo.ToOracleId) + if !ok { + return fmt.Errorf("oracle ID %d is out of range", m.SendTo.ToOracleId) + } if out, ok := c.pbToOutbound(m.SendTo.Msg); ok { s.outboundSizes.record(ctx, len(m.SendTo.Msg.Payload)) - endpoint.SendTo(out, commontypes.OracleID(m.SendTo.ToOracleId)) + endpoint.SendTo(out, to) } case *creproxy.Endpoint2ClientRequest_Broadcast: if out, ok := c.pbToOutbound(m.Broadcast.Msg); ok { @@ -209,6 +217,11 @@ func (c *endpoint2Conn) pbToOutbound(msg *creproxy.OutboundMessage2) (ocr2types. if msg == nil { return nil, false } + // Priority is a byte on libocr's side and a uint32 on the wire, so a value that does not fit + // is refused: truncating it would quietly send the message at a priority nobody asked for. + if msg.Priority > math.MaxUint8 { + return nil, false + } priority := ocr2types.BinaryMessageOutboundPriority(msg.Priority) switch k := msg.Kind.(type) { case *creproxy.OutboundMessage2_Plain: diff --git a/crecore/go.mod b/crecore/go.mod index aadc8b098..0f7551f90 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -3,9 +3,11 @@ module github.com/smartcontractkit/capabilities/crecore go 1.26.2 require ( + github.com/jmoiron/sqlx v1.4.0 github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 @@ -147,7 +149,6 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jmhodges/levigo v1.0.0 // indirect - github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -209,7 +210,6 @@ require ( github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect diff --git a/crecore/main.go b/crecore/main.go index 35ecc9bc8..22d35a4c9 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -2,10 +2,12 @@ package main import ( "context" + "database/sql" "embed" "log" "net" + "github.com/jmoiron/sqlx" "github.com/spf13/cobra" "github.com/smartcontractkit/capabilities/crecore/registry" @@ -13,6 +15,7 @@ import ( "github.com/smartcontractkit/capabilities/libs/standalone/db" "github.com/smartcontractkit/capabilities/libs/standalone/listener" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" "github.com/smartcontractkit/chainlink-evm/pkg/cre/evm" evmregistry "github.com/smartcontractkit/chainlink-evm/pkg/cre/registry" @@ -30,6 +33,10 @@ const migrationsTable = "proxy_migrations" // CREATE TABLE in migrations/0001_*.sql. const ocrDiscovererTable = "proxy_ocr_discoverer_announcements" +// registrySnapshotsTable is where the last known registry is kept, so a restart can answer registry +// lookups before its first on-chain read lands. Must match the CREATE TABLE in migrations/0002_*.sql. +const registrySnapshotsTable = "proxy_registry_snapshots" + // defaultListenAddress is where the proxy and registry are served when no address is configured. const defaultListenAddress = ":50051" @@ -83,15 +90,22 @@ run "docs" to write the full reference to docs/CONFIG.md.`, // per instance keeps that entirely out of the service. listenerDep := listener.Dependency("proxy", defaultListenAddress) - return standalone.Run3(bootstrapper, func( + return standalone.Run4(bootstrapper, func( ctx context.Context, scfg *standalone.StandaloneConfig, factories *ocr.RageFactories, reader registry.Reader, lis net.Listener, + database *sql.DB, ) []services.Service { + // Where the last known registry is kept. Resolving the database again costs nothing - it is + // the same dependency the OCR host already took, opened once - and taking it here is what + // keeps the registry's own table its own business rather than the OCR host's. + regORM := registrysyncer.NewORM(sqlx.NewDb(database, "pgx"), + scfg.Logger.Named("registry snapshots"), registrySnapshotsTable) + regSvc := newRegistryService(cfg.CapabilitiesRegistrySyncInterval.Duration(), - scfg.Logger.Named("capabilities registry"), reader, factories.PeerID) + scfg.Logger.Named("capabilities registry"), reader, regORM, factories.PeerID) // The registry attaches to the proxy's gRPC server, so core reaches both // over the single address it already configures. proxySvc := newProxyService(scfg.Logger.Named("proxy service"), lis, &factories.OCRFactories, regSvc.Register) @@ -99,5 +113,5 @@ run "docs" to write the full reference to docs/CONFIG.md.`, // core running it and this process merely fronting it. dispatcherSvc := newDispatcherService(cfg.Dispatcher, scfg.Logger.Named("dispatcher"), factories, regSvc.CapabilitiesRegistry()) return []services.Service{proxySvc, regSvc, dispatcherSvc} - }, ocrDep, readerDep, listenerDep) + }, ocrDep, readerDep, listenerDep, dbDep) } diff --git a/crecore/migrations/0002_create_proxy_registry_snapshots.sql b/crecore/migrations/0002_create_proxy_registry_snapshots.sql new file mode 100644 index 000000000..21abf607a --- /dev/null +++ b/crecore/migrations/0002_create_proxy_registry_snapshots.sql @@ -0,0 +1,14 @@ +-- +goose Up +-- Registry snapshots, so a restart can answer registry lookups from the last known state while its +-- first on-chain read is still in flight. data is the snapshot as JSON; data_hash is what makes an +-- unchanged registry not write a new row. +CREATE TABLE proxy_registry_snapshots ( + id BIGSERIAL PRIMARY KEY, + data jsonb NOT NULL, + data_hash text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); +-- Only ever read newest-first, and pruned the same way. +CREATE INDEX idx_proxy_registry_snapshots_id_desc ON proxy_registry_snapshots (id DESC); +-- +goose Down +DROP TABLE proxy_registry_snapshots; diff --git a/crecore/registry/local_registry.go b/crecore/registry/local_registry.go index d6b10a71b..f47508e41 100644 --- a/crecore/registry/local_registry.go +++ b/crecore/registry/local_registry.go @@ -1,222 +1,34 @@ // Package registry keeps this binary's view of the CapabilitiesRegistry fresh and answerable: who // the DONs are, which nodes belong to them, which capabilities they host, and which node this is. // -// Where the registry actually lives is not this package's business - a registry.Reader supplies -// whole snapshots from wherever it is written, and the on-chain one lives in chainlink-evm. What is -// here is the Syncer that keeps asking, the snapshot lookups the metadata API needs, and the -// adapter that serves them. +// Where the registry actually lives is not this package's business - a Reader supplies whole +// snapshots from wherever it is written, and the on-chain one lives in chainlink-evm. What is here +// is the Syncer that keeps asking, and the Registry that serves what it finds over gRPC alongside +// the capabilities registered against it. +// +// Holding a snapshot and answering questions about it belongs to neither: that is +// libs/x/registrysyncer, shared with chainlink core so both build against the same code while core +// migrates off its own copy. package registry import ( - "context" - "errors" - "fmt" - "sync" - - ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" commonregistry "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone/registry" - "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" ) -// The registry's own vocabulary comes from the contract a Reader fills in, aliased so this package -// reads as though it owned them. +// The vocabulary a Reader fills in, aliased so this package reads as though it owned it. These are +// the contract's own types - a Reader deals in raw config bytes - and are what a Snapshot is made +// of. type ( - DonID = commonregistry.DonID - DON = commonregistry.DON - NodeInfo = commonregistry.NodeInfo - Capability = commonregistry.Capability - Snapshot = commonregistry.Snapshot - Reader = commonregistry.Reader + DonID = commonregistry.DonID + DON = commonregistry.DON + CapabilityConfiguration = commonregistry.CapabilityConfiguration + NodeInfo = commonregistry.NodeInfo + Capability = commonregistry.Capability + Snapshot = commonregistry.Snapshot + Reader = commonregistry.Reader ) -// LocalRegistry is an immutable snapshot of the on-chain registry, plus the -// lookups the metadata API needs. It is ported from chainlink's -// core/services/registrysyncer.LocalRegistry, minus the ORM/DB persistence and -// with capability config left as raw bytes. -// -// A LocalRegistry is never mutated after construction; Syncer swaps whole -// snapshots. -type LocalRegistry struct { - Logger logger.Logger - GetPeerID func() (ragetypes.PeerID, error) - IDsToDONs map[DonID]DON - IDsToNodes map[ragetypes.PeerID]NodeInfo - IDsToCapabilities map[string]Capability - - cacheMu sync.RWMutex - cachedLocalNodePeer ragetypes.PeerID - cachedLocalNode capabilities.Node -} - -func NewLocalRegistry( - lggr logger.Logger, - getPeerID func() (ragetypes.PeerID, error), - idsToDONs map[DonID]DON, - idsToNodes map[ragetypes.PeerID]NodeInfo, - idsToCapabilities map[string]Capability, -) *LocalRegistry { - return &LocalRegistry{ - Logger: logger.Named(lggr, "LocalRegistry"), - GetPeerID: getPeerID, - IDsToDONs: idsToDONs, - IDsToNodes: idsToNodes, - IDsToCapabilities: idsToCapabilities, - } -} - -// LocalNode resolves this process's own node record. The result is cached: the -// peer ID cannot change at runtime, so a changed peer ID is logged as a bug -// rather than silently accepted. -func (l *LocalRegistry) LocalNode(ctx context.Context) (capabilities.Node, error) { - pid, err := l.GetPeerID() - if err != nil { - return capabilities.Node{}, fmt.Errorf("unable to get local node peer ID: %w", err) - } - - l.cacheMu.RLock() - if l.cachedLocalNodePeer != (ragetypes.PeerID{}) && l.cachedLocalNodePeer == pid { - node := l.cachedLocalNode - l.cacheMu.RUnlock() - return node, nil - } - l.cacheMu.RUnlock() - - l.cacheMu.Lock() - defer l.cacheMu.Unlock() - if l.cachedLocalNodePeer != (ragetypes.PeerID{}) && l.cachedLocalNodePeer == pid { - return l.cachedLocalNode, nil - } - - if l.cachedLocalNodePeer != (ragetypes.PeerID{}) { - l.Logger.Errorw("node's peerID changed at runtime, this should never happen", - "cachedLocalNodePeer", l.cachedLocalNodePeer, "currentPeerID", pid) - } - - n, err := l.NodeByPeerID(ctx, pid) - if err != nil { - return n, err - } - l.cachedLocalNode = n - l.cachedLocalNodePeer = pid - return n, nil -} - -func (l *LocalRegistry) NodeByPeerID(_ context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { - if err := l.ensureNotEmpty(); err != nil { - return capabilities.Node{}, err - } - nodeInfo, ok := l.IDsToNodes[peerID] - if !ok { - return capabilities.Node{}, errors.New("could not find peerID " + peerID.String()) - } - - var workflowDON capabilities.DON - var capabilityDONs []capabilities.DON - for _, d := range l.IDsToDONs { - for _, p := range d.Members { - if p != peerID { - continue - } - if d.AcceptsWorkflows { - // The CapabilitiesRegistry enforces DON ID > 0, so a zero ID here - // means workflowDON has not been assigned yet. - if workflowDON.ID == 0 { - workflowDON = d.DON - } else { - l.Logger.Errorw("Configuration error: node belongs to more than one workflowDON", "peerID", peerID) - } - } - capabilityDONs = append(capabilityDONs, d.DON) - } - } - - return capabilities.Node{ - PeerID: &peerID, - NodeOperatorID: nodeInfo.NodeOperatorID, - Signer: nodeInfo.Signer, - EncryptionPublicKey: nodeInfo.EncryptionPublicKey, - WorkflowDON: workflowDON, - CapabilityDONs: capabilityDONs, - }, nil -} - -func (l *LocalRegistry) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { - if err := l.ensureNotEmpty(); err != nil { - return nil, err - } - - found := []capabilities.DONWithNodes{} - for _, don := range l.IDsToDONs { - if _, ok := don.CapabilityConfigurations[capabilityID]; !ok { - continue - } - nodes, err := l.nodesForDON(ctx, don.DON) - if err != nil { - return nil, fmt.Errorf("could not fetch nodes for DON %d: %w", don.ID, err) - } - found = append(found, capabilities.DONWithNodes{DON: don.DON, Nodes: nodes}) - } - - if len(found) == 0 { - return nil, fmt.Errorf("could not find DON for capability %s", capabilityID) - } - return found, nil -} - -func (l *LocalRegistry) nodesForDON(ctx context.Context, don capabilities.DON) ([]capabilities.Node, error) { - nodes := make([]capabilities.Node, 0, len(don.Members)) - for _, n := range don.Members { - node, err := l.NodeByPeerID(ctx, n) - if err != nil { - return nil, fmt.Errorf("could not find node for peerID %s: %w", n.String(), err) - } - nodes = append(nodes, node) - } - return nodes, nil -} - -// DONByID resolves any DON known to the registry, including caller/workflow DONs -// that host no capability. Callers need this to read a caller DON's Families -// (e.g. zone membership) from its WorkflowDonID. -func (l *LocalRegistry) DONByID(_ context.Context, donID uint32) (capabilities.DON, error) { - if err := l.ensureNotEmpty(); err != nil { - return capabilities.DON{}, err - } - d, ok := l.IDsToDONs[DonID(donID)] - if !ok { - return capabilities.DON{}, fmt.Errorf("could not find don %d", donID) - } - return d.DON, nil -} - -// RawConfigForCapability returns the wire-encoded capabilities/pb.CapabilityConfig -// for a capability on a DON, undecoded. See DON.CapabilityConfigurations. -func (l *LocalRegistry) RawConfigForCapability(_ context.Context, capabilityID string, donID uint32) ([]byte, error) { - if err := l.ensureNotEmpty(); err != nil { - return nil, err - } - d, ok := l.IDsToDONs[DonID(donID)] - if !ok { - return nil, fmt.Errorf("could not find don %d", donID) - } - cfg, ok := d.CapabilityConfigurations[capabilityID] - if !ok { - return nil, fmt.Errorf("could not find capability configuration for capability %s and donID %d", capabilityID, donID) - } - return cfg, nil -} - -func (l *LocalRegistry) ensureNotEmpty() error { - if len(l.IDsToDONs) == 0 { - return errors.New("empty local registry: no DONs registered") - } - if len(l.IDsToNodes) == 0 { - return errors.New("empty local registry: no nodes registered") - } - if len(l.IDsToCapabilities) == 0 { - return errors.New("empty local registry: no capabilities registered") - } - return nil -} +// LocalRegistry is one snapshot, and the lookups over it. +type LocalRegistry = registrysyncer.LocalRegistry diff --git a/crecore/registry/metadata.go b/crecore/registry/metadata.go deleted file mode 100644 index 6da05778f..000000000 --- a/crecore/registry/metadata.go +++ /dev/null @@ -1,62 +0,0 @@ -package registry - -import ( - "context" - - ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - regserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/server" -) - -// Metadata adapts the Syncer to regserver.MetadataSource. -// -// The registry and its wire adapter live in chainlink-common; only the on-chain -// read lives here, because only it needs an EVM client and contract bindings. -// Each call resolves the current snapshot, so a sync that lands between calls is -// picked up without the registry holding a stale view. -func (s *Syncer) Metadata() regserver.MetadataSource { return metadataSource{s} } - -type metadataSource struct{ s *Syncer } - -var _ regserver.MetadataSource = metadataSource{} - -func (m metadataSource) LocalNode(ctx context.Context) (capabilities.Node, error) { - lr, err := m.s.Current() - if err != nil { - return capabilities.Node{}, err - } - return lr.LocalNode(ctx) -} - -func (m metadataSource) NodeByPeerID(ctx context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { - lr, err := m.s.Current() - if err != nil { - return capabilities.Node{}, err - } - return lr.NodeByPeerID(ctx, peerID) -} - -func (m metadataSource) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { - lr, err := m.s.Current() - if err != nil { - return nil, err - } - return lr.DONsForCapability(ctx, capabilityID) -} - -func (m metadataSource) DONByID(ctx context.Context, donID uint32) (capabilities.DON, error) { - lr, err := m.s.Current() - if err != nil { - return capabilities.DON{}, err - } - return lr.DONByID(ctx, donID) -} - -func (m metadataSource) RawConfigForCapability(ctx context.Context, capabilityID string, donID uint32) ([]byte, error) { - lr, err := m.s.Current() - if err != nil { - return nil, err - } - return lr.RawConfigForCapability(ctx, capabilityID, donID) -} diff --git a/crecore/registry/registry.go b/crecore/registry/registry.go new file mode 100644 index 000000000..ebbad81a8 --- /dev/null +++ b/crecore/registry/registry.go @@ -0,0 +1,326 @@ +package registry + +import ( + "context" + "errors" + "fmt" + "sort" + "sync" + + "google.golang.org/grpc" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + baseregistry "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// Handle is a registered capability: its ID, which capability services it +// serves, and the gRPC target serving them. +// +// Unlike the go-plugin registry, a Handle holds no connection and owns no +// resources, so registering and resolving a capability allocates nothing that +// has to be reclaimed. Reachability of URL is the registrant's responsibility +// for as long as the capability stays registered. +type Handle struct { + ID string + Type capabilities.CapabilityType + URL string +} + +// Registry is the CapabilitiesRegistry this binary serves. +// +// It composes two independent sources: +// +// - Handles, registered at runtime over AddHandle by the processes that host the capabilities. +// AddHandle dials the handle's address itself and wraps it into a real capabilities.BaseCapability +// (the same conversion registry/client does for its own callers), then adds that to local, +// chainlink's ordinary in-process base registry. AddHandle is a drop-in replacement for the +// in-process Add(BaseCapability) core uses without a proxy: a capability registered this way is +// just as real and callable as one added directly, whether the caller asking for it is in this +// process or another one reached the normal way, over the gRPC service Get/GetTrigger/GetExecutable +// wrap. +// - Metadata, read from whichever snapshot is current when a call arrives. Read-only from here; +// whoever supplies the snapshots decides how they are refreshed. +// +// Registry also implements core.CapabilitiesRegistry directly (see the passthrough +// methods below), so a dispatcher in this same process can call it exactly like any +// other in-process registry: the Handle-addressed API above exists for the gRPC +// service instead, so the two never share a method name. +// +// Registry is safe for concurrent use. +type Registry struct { + lggr logger.Logger + + // snapshot resolves the registry as it currently stands. It is a function rather than a stored + // value because a snapshot is replaced wholesale on every sync, and because there is nothing to + // answer with before the first one lands - which it reports as an error, so metadata calls fail + // with a reason instead of with an empty registry. + snapshot func() (*LocalRegistry, error) + + // local holds the real, callable value AddHandle resolves a Handle to. Anything in this process + // that wants to call a capability - locally added, or reached only by dialing a Handle's address - + // goes through this, not through handles below. + local core.CapabilitiesRegistryBase + + // dialOpts are applied when dialing a Handle's address to resolve it into local. + dialOpts []grpc.DialOption + + mu sync.RWMutex + handles map[string]Handle + conns map[string]*grpc.ClientConn // by URL; closed and dropped on Remove +} + +// New returns a Registry answering metadata from whatever snapshot resolves at the time of the +// call - in this binary, the Syncer's. +func New(lggr logger.Logger, snapshot func() (*LocalRegistry, error), dialOpts ...grpc.DialOption) *Registry { + return &Registry{ + lggr: logger.Named(lggr, "CapabilitiesRegistry"), + snapshot: snapshot, + local: baseregistry.NewBaseRegistry(lggr), + dialOpts: dialOpts, + handles: map[string]Handle{}, + conns: map[string]*grpc.ClientConn{}, + } +} + +// Local is the real, in-process registry AddHandle resolves Handles into: whatever in this process +// wants to actually call a capability - rather than tell some other process where to find it - +// uses this. +func (r *Registry) Local() core.CapabilitiesRegistryBase { return r.local } + +// AddHandle registers a capability served at url. +// +// Re-registration is expected rather than exceptional, so it is accepted in both +// forms: +// +// - Same address as the existing entry: nothing to change, so this succeeds +// without touching the registry. A capability that serves itself at a fixed +// address re-announces that same address every time it restarts, and failing +// the second registration would take the capability out of service even though +// the entry already points exactly where it should. +// - Different address: the host moved, so the entry is repointed. Requiring a +// Remove first would lose the capability for the window between the two calls. +// +// This registry deliberately does not try to distinguish a restart from a second +// process claiming the same ID. It cannot: it holds an address, not a connection, +// so it has no way to tell whether the existing entry is still alive. Ownership of +// a capability ID is settled on chain, and a caller that wants the stricter rule +// has the liveness information locally — see how core gates replacement on +// connection state in chainlink-common's baseRegistry. +func (r *Registry) AddHandle(ctx context.Context, h Handle) error { + if h.ID == "" { + return errors.New("capability ID is required") + } + if h.URL == "" { + return fmt.Errorf("callback URL is required to register capability %s", h.ID) + } + + r.mu.Lock() + defer r.mu.Unlock() + + if prev, ok := r.handles[h.ID]; ok && prev.URL == h.URL && prev.Type == h.Type { + r.lggr.Debugw("capability re-registered at the same address; nothing to do", + "capabilityID", h.ID, "url", h.URL) + return nil + } + + conn, err := grpc.NewClient(h.URL, r.dialOpts...) + if err != nil { + return fmt.Errorf("failed to dial capability %s at %s: %w", h.ID, h.URL, err) + } + wrapped, err := client.Wrap(r.lggr, conn, h.Type) + if err != nil { + _ = conn.Close() + return fmt.Errorf("failed to wrap capability %s at %s: %w", h.ID, h.URL, err) + } + + // The wrapped value cannot report connection state (it is a plain RPC client, not a + // *grpc.ClientConn), so local's own liveness-gated replace never fires - drop the previous + // entry explicitly instead so a moved capability is replaced rather than rejected. + if prevConn, ok := r.conns[h.ID]; ok { + _ = r.local.Remove(ctx, h.ID) + _ = prevConn.Close() + } + if err := r.local.Add(ctx, wrapped); err != nil { + _ = conn.Close() + return fmt.Errorf("failed to add capability %s to local registry: %w", h.ID, err) + } + r.conns[h.ID] = conn + + if prev, ok := r.handles[h.ID]; ok { + r.lggr.Infow("re-registering capability at a new address", + "capabilityID", h.ID, "previousURL", prev.URL, "url", h.URL) + } + r.handles[h.ID] = h + r.lggr.Infow("capability registered", "capabilityID", h.ID, "type", h.Type, "url", h.URL) + return nil +} + +func (r *Registry) RemoveHandle(ctx context.Context, id string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if conn, ok := r.conns[id]; ok { + _ = r.local.Remove(ctx, id) + _ = conn.Close() + delete(r.conns, id) + } + + if _, ok := r.handles[id]; !ok { + return fmt.Errorf("capability %s not found", id) + } + delete(r.handles, id) + r.lggr.Infow("capability removed", "capabilityID", id) + return nil +} + +// GetHandle resolves a capability of any type. +func (r *Registry) GetHandle(_ context.Context, id string) (Handle, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + h, ok := r.handles[id] + if !ok { + return Handle{}, fmt.Errorf("capability %s not found", id) + } + return h, nil +} + +// GetTriggerHandle resolves a capability that must serve TriggerExecutable. +func (r *Registry) GetTriggerHandle(ctx context.Context, id string) (Handle, error) { + h, err := r.GetHandle(ctx, id) + if err != nil { + return Handle{}, err + } + switch h.Type { + case capabilities.CapabilityTypeTrigger, capabilities.CapabilityTypeCombined: + return h, nil + default: + return Handle{}, fmt.Errorf("capability %s is a %s, not a trigger", id, h.Type) + } +} + +// GetExecutableHandle resolves a capability that must serve Executable. +func (r *Registry) GetExecutableHandle(ctx context.Context, id string) (Handle, error) { + h, err := r.GetHandle(ctx, id) + if err != nil { + return Handle{}, err + } + switch h.Type { + case capabilities.CapabilityTypeAction, + capabilities.CapabilityTypeTarget, + capabilities.CapabilityTypeConsensus, + capabilities.CapabilityTypeCombined: + return h, nil + default: + return Handle{}, fmt.Errorf("capability %s is a %s, not executable", id, h.Type) + } +} + +// ListHandles returns every registered capability, ordered by ID so callers and +// tests see a stable sequence. +func (r *Registry) ListHandles(_ context.Context) []Handle { + r.mu.RLock() + defer r.mu.RUnlock() + + out := make([]Handle, 0, len(r.handles)) + for _, h := range r.handles { + out = append(out, h) + } + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + return out +} + +// --- metadata, delegated to whichever snapshot is current --- +// +// Each call resolves the snapshot again rather than holding one, so a sync that lands between two +// calls is picked up without this ever serving a view it has outlived. + +func (r *Registry) LocalNode(ctx context.Context) (capabilities.Node, error) { + lr, err := r.snapshot() + if err != nil { + return capabilities.Node{}, err + } + return lr.LocalNode(ctx) +} + +func (r *Registry) NodeByPeerID(ctx context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { + lr, err := r.snapshot() + if err != nil { + return capabilities.Node{}, err + } + return lr.NodeByPeerID(ctx, peerID) +} + +func (r *Registry) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { + lr, err := r.snapshot() + if err != nil { + return nil, err + } + return lr.DONsForCapability(ctx, capabilityID) +} + +func (r *Registry) DONByID(ctx context.Context, donID uint32) (capabilities.DON, error) { + lr, err := r.snapshot() + if err != nil { + return capabilities.DON{}, err + } + return lr.DONByID(ctx, donID) +} + +// RawConfigForCapability returns the undecoded capability config bytes; see +// DON.CapabilityConfigurations for why they are not decoded here. +func (r *Registry) RawConfigForCapability(ctx context.Context, capabilityID string, donID uint32) ([]byte, error) { + lr, err := r.snapshot() + if err != nil { + return nil, err + } + return lr.RawConfigForCapability(ctx, capabilityID, donID) +} + +// --- core.CapabilitiesRegistry, delegated to local / RawConfigForCapability --- +// +// These give the dispatcher in this same process a plain in-process registry to +// call, backed by the same local map and metadata a gRPC caller reaches through +// Handle/Server. Named the same as core.CapabilitiesRegistry's methods, distinct +// from the Handle-returning methods above, so one Registry answers both without a +// wrapping adapter type. +var _ core.CapabilitiesRegistry = (*Registry)(nil) + +func (r *Registry) Add(ctx context.Context, c capabilities.BaseCapability) error { + return r.local.Add(ctx, c) +} + +func (r *Registry) Remove(ctx context.Context, id string) error { + return r.local.Remove(ctx, id) +} + +func (r *Registry) Get(ctx context.Context, id string) (capabilities.BaseCapability, error) { + return r.local.Get(ctx, id) +} + +func (r *Registry) GetTrigger(ctx context.Context, id string) (capabilities.TriggerCapability, error) { + return r.local.GetTrigger(ctx, id) +} + +func (r *Registry) GetExecutable(ctx context.Context, id string) (capabilities.ExecutableCapability, error) { + return r.local.GetExecutable(ctx, id) +} + +func (r *Registry) List(ctx context.Context) ([]capabilities.BaseCapability, error) { + return r.local.List(ctx) +} + +// ConfigForCapability returns the decoded config, where the gRPC service's own ConfigForCapability +// RPC hands back the bytes for its caller to decode. Both end up in the same decoder. +func (r *Registry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { + lr, err := r.snapshot() + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + return lr.ConfigForCapability(ctx, capabilityID, donID) +} diff --git a/crecore/registry/server.go b/crecore/registry/server.go new file mode 100644 index 000000000..b8867fe92 --- /dev/null +++ b/crecore/registry/server.go @@ -0,0 +1,213 @@ +package registry + +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/emptypb" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + registrypb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/pb" +) + +// Server adapts Registry to the plain-gRPC CapabilitiesRegistry service. +// +// It is a thin translation layer: no broker, no per-capability sub-servers, no +// resource lifetimes. Handle replies carry a URL the caller dials itself. +type Server struct { + registrypb.UnimplementedCapabilitiesRegistryServer + + impl *Registry +} + +var _ registrypb.CapabilitiesRegistryServer = (*Server)(nil) + +func NewServer(impl *Registry) *Server { return &Server{impl: impl} } + +// Register registers this server on a plain grpc.Server. +func Register(s *grpc.Server, impl *Registry) { + registrypb.RegisterCapabilitiesRegistryServer(s, NewServer(impl)) +} + +func (s *Server) Add(ctx context.Context, req *registrypb.AddRequest) (*emptypb.Empty, error) { + capType, err := capabilitiespb.CapabilityTypeFromProto(req.GetType()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if capType == capabilities.CapabilityTypeUnknown { + return nil, status.Errorf(codes.InvalidArgument, + "capability %s has no capability type, so the registry cannot know which services it serves", req.GetCapabilityId()) + } + h := Handle{ID: req.GetCapabilityId(), Type: capType, URL: req.GetCallbackUrl()} + if err := s.impl.AddHandle(ctx, h); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &emptypb.Empty{}, nil +} + +func (s *Server) Remove(ctx context.Context, req *registrypb.RemoveRequest) (*emptypb.Empty, error) { + if err := s.impl.RemoveHandle(ctx, req.GetCapabilityId()); err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + return &emptypb.Empty{}, nil +} + +// handle resolves a capability through lookup and converts it for the wire. The +// three Get* RPCs differ only in which lookup they use. +func handle(ctx context.Context, id string, lookup func(context.Context, string) (Handle, error)) (*registrypb.CapabilityHandle, error) { + h, err := lookup(ctx, id) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + return handleToProto(h) +} + +func (s *Server) Get(ctx context.Context, req *registrypb.GetRequest) (*registrypb.CapabilityHandle, error) { + return handle(ctx, req.GetCapabilityId(), s.impl.GetHandle) +} + +func (s *Server) GetTrigger(ctx context.Context, req *registrypb.GetRequest) (*registrypb.CapabilityHandle, error) { + return handle(ctx, req.GetCapabilityId(), s.impl.GetTriggerHandle) +} + +func (s *Server) GetExecutable(ctx context.Context, req *registrypb.GetRequest) (*registrypb.CapabilityHandle, error) { + return handle(ctx, req.GetCapabilityId(), s.impl.GetExecutableHandle) +} + +func (s *Server) List(ctx context.Context, _ *emptypb.Empty) (*registrypb.ListReply, error) { + handles := s.impl.ListHandles(ctx) + out := make([]*registrypb.CapabilityHandle, 0, len(handles)) + for _, h := range handles { + ph, err := handleToProto(h) + if err != nil { + return nil, err + } + out = append(out, ph) + } + return ®istrypb.ListReply{Handles: out}, nil +} + +func (s *Server) LocalNode(ctx context.Context, _ *emptypb.Empty) (*registrypb.NodeReply, error) { + n, err := s.impl.LocalNode(ctx) + if err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + return nodeToProto(n), nil +} + +func (s *Server) NodeByPeerID(ctx context.Context, req *registrypb.NodeRequest) (*registrypb.NodeReply, error) { + peerID, err := toPeerID(req.GetPeerId()) + if err != nil { + return nil, err + } + n, err := s.impl.NodeByPeerID(ctx, peerID) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + return nodeToProto(n), nil +} + +func (s *Server) ConfigForCapability(ctx context.Context, req *registrypb.ConfigForCapabilityRequest) (*registrypb.ConfigForCapabilityReply, error) { + raw, err := s.impl.RawConfigForCapability(ctx, req.GetCapabilityId(), req.GetDonId()) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + + // The contract stores a wire-encoded CapabilityConfig, so this only parses the + // bytes into the shared message; none of the config is interpreted here. + cfg := &capabilitiespb.CapabilityConfig{} + if err := proto.Unmarshal(raw, cfg); err != nil { + return nil, status.Errorf(codes.Internal, + "capability %s on DON %d has an unparseable on-chain config: %s", + req.GetCapabilityId(), req.GetDonId(), err) + } + return ®istrypb.ConfigForCapabilityReply{CapabilityConfig: cfg}, nil +} + +func (s *Server) DONsForCapability(ctx context.Context, req *registrypb.DONsForCapabilityRequest) (*registrypb.DONsForCapabilityReply, error) { + dons, err := s.impl.DONsForCapability(ctx, req.GetCapabilityId()) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + out := make([]*registrypb.DONWithNodes, 0, len(dons)) + for _, d := range dons { + nodes := make([]*registrypb.NodeReply, 0, len(d.Nodes)) + for _, n := range d.Nodes { + nodes = append(nodes, nodeToProto(n)) + } + out = append(out, ®istrypb.DONWithNodes{Don: donToProto(d.DON), Nodes: nodes}) + } + return ®istrypb.DONsForCapabilityReply{Dons: out}, nil +} + +func (s *Server) DONByID(ctx context.Context, req *registrypb.DONByIDRequest) (*registrypb.DONByIDReply, error) { + d, err := s.impl.DONByID(ctx, req.GetDonId()) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + return ®istrypb.DONByIDReply{Don: donToProto(d)}, nil +} + +// --- conversions --- + +func handleToProto(h Handle) (*registrypb.CapabilityHandle, error) { + pbType, err := capabilitiespb.CapabilityTypeToProto(h.Type) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return ®istrypb.CapabilityHandle{ + CapabilityId: h.ID, + Type: pbType, + CallbackUrl: h.URL, + }, nil +} + +func donToProto(d capabilities.DON) *registrypb.DON { + members := make([][]byte, len(d.Members)) + for i, m := range d.Members { + members[i] = m[:] + } + return ®istrypb.DON{ + Id: d.ID, + Name: d.Name, + Members: members, + F: uint32(d.F), + ConfigVersion: d.ConfigVersion, + Families: d.Families, + Config: d.Config, + IsPublic: d.IsPublic, + AcceptsWorkflows: d.AcceptsWorkflows, + } +} + +func nodeToProto(n capabilities.Node) *registrypb.NodeReply { + reply := ®istrypb.NodeReply{ + WorkflowDon: donToProto(n.WorkflowDON), + NodeOperatorId: n.NodeOperatorID, + Signer: n.Signer[:], + EncryptionPublicKey: n.EncryptionPublicKey[:], + } + if n.PeerID != nil { + reply.PeerId = n.PeerID[:] + } + for _, d := range n.CapabilityDONs { + reply.CapabilityDons = append(reply.CapabilityDons, donToProto(d)) + } + return reply +} + +func toPeerID(b []byte) (ragetypes.PeerID, error) { + var peerID ragetypes.PeerID + if len(b) != len(peerID) { + return peerID, status.Errorf(codes.InvalidArgument, + "invalid peer ID length: got %d, want %d", len(b), len(peerID)) + } + copy(peerID[:], b) + return peerID, nil +} diff --git a/crecore/registry/server_test.go b/crecore/registry/server_test.go new file mode 100644 index 000000000..ee13e2a8f --- /dev/null +++ b/crecore/registry/server_test.go @@ -0,0 +1,707 @@ +// Package registry_test, not registry: this drives the real registry and its real +// gRPC adapter against chainlink-common's client package over an in-memory +// listener, so it needs both from outside either package. +package registry_test + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "testing" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" + registrypb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + + "github.com/smartcontractkit/capabilities/crecore/registry" + "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" +) + +// These tests drive the real registry and its real gRPC adapter over an in-memory +// listener. Only the on-chain metadata read is substituted, since a test cannot +// run a chain; everything else the client talks to is production code. + +// newRegistry builds the registry under test. AddHandle now dials whatever address it is given to +// resolve a real value, so the registry needs the same capability dial options the client tests +// pass it - book, when the test serves fakes through one; otherwise, just credentials, since +// AddHandle's dial-out is a production requirement, not conditional on what the test's own client +// does. +func newRegistry(t *testing.T, book *AddrBook) *registry.Registry { + t.Helper() + return newRegistryWith(t, book, notSynced) +} + +// notSynced is the snapshot source for the tests that never publish one: metadata calls then fail +// exactly as they do before a process's first read lands. +func notSynced() (*registry.LocalRegistry, error) { + return nil, errors.New("registry not synced yet") +} + +// newRegistryWith is newRegistry against a given snapshot, for the metadata tests. +func newRegistryWith(t *testing.T, book *AddrBook, snapshot func() (*registry.LocalRegistry, error)) *registry.Registry { + t.Helper() + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + if book != nil { + opts = append(opts, book.DialOption()) + } + return registry.New(logger.Test(t), snapshot, opts...) +} + +// snapshotOf serves lr as the current snapshot. +// +// The tests below build real LocalRegistry values rather than a fake metadata source: the lookups +// over a snapshot are production code, so a test that supplies DONs and nodes exercises them +// instead of a stub that could answer differently. +func snapshotOf(lr *registry.LocalRegistry) func() (*registry.LocalRegistry, error) { + return func() (*registry.LocalRegistry, error) { return lr, nil } +} + +// snapshot builds the current snapshot from what a Reader would have returned, through the same +// conversion the syncer uses, with self as the node asking. +func snapshot(t *testing.T, self ragetypes.PeerID, snap *registry.Snapshot) func() (*registry.LocalRegistry, error) { + t.Helper() + return snapshotOf(registrysyncer.FromSnapshot(logger.Test(t), + func() (ragetypes.PeerID, error) { return self, nil }, snap)) +} + +// newClient serves reg and returns a client for it, with capability dials +// resolved through book when one is given. +func newClient(t *testing.T, reg *registry.Registry, book *AddrBook) *client.Client { + t.Helper() + + var opts []grpc.DialOption + if book != nil { + opts = append(opts, book.DialOption(), grpc.WithTransportCredentials(insecure.NewCredentials())) + } + return client.New(logger.Test(t), Serve(t, reg), opts...) +} + +// serveCapabilityAt registers impl in book under name and returns its address. +func serveCapabilityAt(t *testing.T, book *AddrBook, name string, + impl capabilities.BaseCapability, capType capabilities.CapabilityType) string { + t.Helper() + + srv := grpc.NewServer() + require.NoError(t, client.RegisterCapability(logger.Test(t), srv, impl, capType)) + return book.Serve(t, name, srv) +} + +func peer(b byte) ragetypes.PeerID { + var p ragetypes.PeerID + p[0] = b + return p +} + +// fakeExecutable and fakeTrigger are local test doubles for the capability +// surfaces the registry serves. + +type fakeExecutable struct { + info capabilities.CapabilityInfo + + executeErr error + response capabilities.CapabilityResponse + + registeredTo []capabilities.RegistrationMetadata + unregisteredTo []capabilities.RegistrationMetadata + lastRequest capabilities.CapabilityRequest +} + +func (f *fakeExecutable) Info(context.Context) (capabilities.CapabilityInfo, error) { + return f.info, nil +} + +func (f *fakeExecutable) Execute(_ context.Context, req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + f.lastRequest = req + if f.executeErr != nil { + return capabilities.CapabilityResponse{}, f.executeErr + } + return f.response, nil +} + +func (f *fakeExecutable) RegisterToWorkflow(_ context.Context, req capabilities.RegisterToWorkflowRequest) error { + f.registeredTo = append(f.registeredTo, req.Metadata) + return nil +} + +func (f *fakeExecutable) UnregisterFromWorkflow(_ context.Context, req capabilities.UnregisterFromWorkflowRequest) error { + f.unregisteredTo = append(f.unregisteredTo, req.Metadata) + return nil +} + +type fakeTrigger struct { + info capabilities.CapabilityInfo + + registerErr error + events []capabilities.TriggerResponse + + unregistered chan capabilities.TriggerRegistrationRequest + acked chan [3]string +} + +func newFakeTrigger(info capabilities.CapabilityInfo) *fakeTrigger { + return &fakeTrigger{ + info: info, + unregistered: make(chan capabilities.TriggerRegistrationRequest, 4), + acked: make(chan [3]string, 4), + } +} + +func (f *fakeTrigger) Info(context.Context) (capabilities.CapabilityInfo, error) { + return f.info, nil +} + +func (f *fakeTrigger) RegisterTrigger(_ context.Context, _ capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + if f.registerErr != nil { + return nil, f.registerErr + } + ch := make(chan capabilities.TriggerResponse, len(f.events)) + for _, e := range f.events { + ch <- e + } + close(ch) + return ch, nil +} + +func (f *fakeTrigger) UnregisterTrigger(_ context.Context, req capabilities.TriggerRegistrationRequest) error { + select { + case f.unregistered <- req: + default: + } + return nil +} + +func (f *fakeTrigger) AckEvent(_ context.Context, triggerID, eventID, method string) error { + select { + case f.acked <- [3]string{triggerID, eventID, method}: + default: + } + return nil +} + +// --- registration --- + +func TestAddAt_CarriesCapabilityTypeVerbatim(t *testing.T) { + ctx := context.Background() + + // The wire carries the capability's own type, so nothing is collapsed on the + // way out and nothing has to be guessed back on the way in. + for _, capType := range []capabilities.CapabilityType{ + capabilities.CapabilityTypeTrigger, + capabilities.CapabilityTypeAction, + capabilities.CapabilityTypeTarget, + capabilities.CapabilityTypeConsensus, + capabilities.CapabilityTypeCombined, + } { + t.Run(string(capType), func(t *testing.T) { + book := NewAddrBook() + info := capabilities.MustNewCapabilityInfo("c@1.0.0", capType, "c") + impl := &fakeCombined{ + fakeExecutable: &fakeExecutable{info: info}, + fakeTrigger: newFakeTrigger(info), + } + addr := serveCapabilityAt(t, book, "c", impl, capType) + + reg := newRegistry(t, book) + c := newClient(t, reg, book) + + require.NoError(t, c.AddAt(ctx, "c@1.0.0", capType, addr)) + + got, err := reg.GetHandle(ctx, "c@1.0.0") + require.NoError(t, err) + assert.Equal(t, capType, got.Type) + assert.Equal(t, addr, got.URL) + }) + } +} + +func TestAddAt_RejectsUnknownType(t *testing.T) { + ctx := context.Background() + + reg := newRegistry(t, nil) + c := newClient(t, reg, nil) + + // Without a type the registry cannot know which services live at the address. + require.Error(t, c.AddAt(ctx, "c@1.0.0", capabilities.CapabilityTypeUnknown, "addr:1")) + assert.Empty(t, reg.ListHandles(ctx)) +} + +func TestAddAt_RejectsEmptyAddress(t *testing.T) { + ctx := context.Background() + + reg := newRegistry(t, nil) + c := newClient(t, reg, nil) + + require.Error(t, c.AddAt(ctx, "act@1.0.0", capabilities.CapabilityTypeAction, "")) + assert.Empty(t, reg.ListHandles(ctx)) +} + +func TestServer_Remove(t *testing.T) { + ctx := context.Background() + + book := NewAddrBook() + impl := &fakeExecutable{ + info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), + } + addr := serveCapabilityAt(t, book, "cap-a", impl, capabilities.CapabilityTypeAction) + + reg := newRegistry(t, book) + c := newClient(t, reg, book) + + require.NoError(t, c.AddAt(ctx, "act@1.0.0", capabilities.CapabilityTypeAction, addr)) + require.NoError(t, c.Remove(ctx, "act@1.0.0")) + + _, err := reg.GetHandle(ctx, "act@1.0.0") + require.Error(t, err) + + // Removing what is not there is an error, not a silent no-op. + require.Error(t, c.Remove(ctx, "act@1.0.0")) +} + +// --- resolution --- + +func TestServer_GetExecutableDialsHandleAddress(t *testing.T) { + ctx := context.Background() + + outputs, err := values.NewMap(map[string]any{"out": int64(3)}) + require.NoError(t, err) + + book := NewAddrBook() + impl := &fakeExecutable{ + info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), + response: capabilities.CapabilityResponse{Value: outputs}, + } + addr := serveCapabilityAt(t, book, "cap-a", impl, capabilities.CapabilityTypeAction) + + reg := newRegistry(t, book) + c := newClient(t, reg, book) + require.NoError(t, c.AddAt(ctx, "act@1.0.0", capabilities.CapabilityTypeAction, addr)) + + exec, err := c.GetExecutable(ctx, "act@1.0.0") + require.NoError(t, err) + + resp, err := exec.Execute(ctx, capabilities.CapabilityRequest{}) + require.NoError(t, err) + assert.Equal(t, outputs, resp.Value) +} + +func TestServer_ReusesConnectionPerAddress(t *testing.T) { + ctx := context.Background() + + book := NewAddrBook() + impl := &fakeExecutable{ + info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), + } + addr := serveCapabilityAt(t, book, "cap-a", impl, capabilities.CapabilityTypeAction) + + reg := newRegistry(t, book) + c := newClient(t, reg, book) + require.NoError(t, c.AddAt(ctx, "act@1.0.0", capabilities.CapabilityTypeAction, addr)) + // AddHandle itself dials once, server-side, to resolve the handle into a real value. + dialsBeforeLookups := book.DialCount("cap-a") + + // GetExecutable is on the per-invocation path for workflow steps, so a fresh + // dial per call would add a handshake to every capability call. + for i := 0; i < 5; i++ { + exec, err := c.GetExecutable(ctx, "act@1.0.0") + require.NoError(t, err) + _, err = exec.Execute(ctx, capabilities.CapabilityRequest{}) + require.NoError(t, err) + } + + assert.Equal(t, dialsBeforeLookups+1, book.DialCount("cap-a"), "expected one transport dial across repeated lookups") +} + +func TestServer_GetTriggerRejectsExecutableOnlyCapability(t *testing.T) { + ctx := context.Background() + + book := NewAddrBook() + impl := &fakeExecutable{ + info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), + } + addr := serveCapabilityAt(t, book, "cap-a", impl, capabilities.CapabilityTypeAction) + + reg := newRegistry(t, book) + c := newClient(t, reg, book) + require.NoError(t, c.AddAt(ctx, "act@1.0.0", capabilities.CapabilityTypeAction, addr)) + + // The registry gates this server-side, so the client never even gets a handle. + _, err := c.GetTrigger(ctx, "act@1.0.0") + require.Error(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestServer_CombinedCapabilityServesBothSurfaces(t *testing.T) { + ctx := context.Background() + + book := NewAddrBook() + info := capabilities.MustNewCapabilityInfo("both@1.0.0", capabilities.CapabilityTypeCombined, "both") + impl := &fakeCombined{ + fakeExecutable: &fakeExecutable{info: info}, + fakeTrigger: newFakeTrigger(info), + } + addr := serveCapabilityAt(t, book, "cap-b", impl, capabilities.CapabilityTypeCombined) + + reg := newRegistry(t, book) + c := newClient(t, reg, book) + require.NoError(t, c.AddAt(ctx, "both@1.0.0", capabilities.CapabilityTypeCombined, addr)) + + _, err := c.GetExecutable(ctx, "both@1.0.0") + require.NoError(t, err) + _, err = c.GetTrigger(ctx, "both@1.0.0") + require.NoError(t, err) +} + +func TestServer_GetUnknownCapability(t *testing.T) { + ctx := context.Background() + + c := newClient(t, newRegistry(t, nil), nil) + + _, err := c.Get(ctx, "nope@1.0.0") + require.Error(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestServer_List(t *testing.T) { + ctx := context.Background() + + book := NewAddrBook() + impl := &fakeExecutable{ + info: capabilities.MustNewCapabilityInfo("good@1.0.0", capabilities.CapabilityTypeAction, "good"), + } + addr := serveCapabilityAt(t, book, "cap-good", impl, capabilities.CapabilityTypeAction) + + reg := newRegistry(t, book) + c := newClient(t, reg, book) + require.NoError(t, c.AddAt(ctx, "good@1.0.0", capabilities.CapabilityTypeAction, addr)) + + got, err := c.List(ctx) + require.NoError(t, err) + require.Len(t, got, 1) + + info, err := got[0].Info(ctx) + require.NoError(t, err) + assert.Equal(t, "good@1.0.0", info.ID) +} + +func TestServer_MissingCredentialsFailsRatherThanFallingBackToInsecure(t *testing.T) { + ctx := context.Background() + + book := NewAddrBook() + impl := &fakeExecutable{ + info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), + } + addr := serveCapabilityAt(t, book, "cap-a", impl, capabilities.CapabilityTypeAction) + + // No capability dial options, so no transport credentials. gRPC must refuse the + // dial: defaulting to insecure would silently make every capability call + // unauthenticated and unencrypted for a caller that forgot to say. + reg := newRegistry(t, book) + c := newClient(t, reg, nil) + require.NoError(t, c.AddAt(ctx, "act@1.0.0", capabilities.CapabilityTypeAction, addr)) + + _, err := c.GetExecutable(ctx, "act@1.0.0") + require.Error(t, err) + assert.Contains(t, err.Error(), "transport security", + "expected gRPC's no-transport-security error, not a successful insecure dial") +} + +// --- metadata --- + +func TestServer_LocalNodeAndNodeByPeerID(t *testing.T) { + ctx := context.Background() + + self := peer(0xab) + reg := newRegistryWith(t, nil, snapshot(t, self, ®istry.Snapshot{ + DONs: map[registry.DonID]registry.DON{ + 1: {DON: capabilities.DON{ + ID: 1, Name: "wf", F: 1, ConfigVersion: 4, + Members: []ragetypes.PeerID{self}, + Families: []string{"zone-a"}, + AcceptsWorkflows: true, + }}, + 2: {DON: capabilities.DON{ + ID: 2, Name: "cap", F: 2, ConfigVersion: 5, + Members: []ragetypes.PeerID{self}, + }}, + }, + Nodes: map[ragetypes.PeerID]registry.NodeInfo{ + self: {NodeOperatorID: 9, P2pID: self, Signer: [32]byte{0xcd}, EncryptionPublicKey: [32]byte{0xef}}, + }, + Capabilities: map[string]registry.Capability{"act@1.0.0": {ID: "act@1.0.0"}}, + })) + c := newClient(t, reg, nil) + + // Round-trips through the real Go -> proto -> Go conversions on both sides. + got, err := c.LocalNode(ctx) + require.NoError(t, err) + require.NotNil(t, got.PeerID) + assert.Equal(t, self, *got.PeerID) + assert.Equal(t, uint32(9), got.NodeOperatorID) + assert.Equal(t, [32]byte{0xcd}, got.Signer) + assert.Equal(t, [32]byte{0xef}, got.EncryptionPublicKey) + assert.Equal(t, uint32(1), got.WorkflowDON.ID) + assert.Equal(t, uint8(1), got.WorkflowDON.F) + assert.Equal(t, uint32(4), got.WorkflowDON.ConfigVersion) + assert.Equal(t, []string{"zone-a"}, got.WorkflowDON.Families) + assert.True(t, got.WorkflowDON.AcceptsWorkflows) + require.Len(t, got.WorkflowDON.Members, 1) + assert.Equal(t, self, got.WorkflowDON.Members[0]) + + // Every DON the node belongs to, the workflow one included. + require.Len(t, got.CapabilityDONs, 2) + ids := []uint32{got.CapabilityDONs[0].ID, got.CapabilityDONs[1].ID} + assert.ElementsMatch(t, []uint32{1, 2}, ids) + + byPeer, err := c.NodeByPeerID(ctx, self) + require.NoError(t, err) + assert.Equal(t, got.NodeOperatorID, byPeer.NodeOperatorID) +} + +func TestServer_MetadataBeforeFirstSync(t *testing.T) { + ctx := context.Background() + + // Metadata is a live read: before a snapshot lands there is no answer to cache or fabricate, + // and the caller has to see that. + c := newClient(t, newRegistry(t, nil), nil) + + _, err := c.LocalNode(ctx) + require.Error(t, err) + assert.Equal(t, codes.FailedPrecondition, status.Code(err)) +} + +// configSnapshot is a snapshot whose DON 1 holds raw as the stored config for vault@1.0.0. +func configSnapshot(t *testing.T, raw []byte) func() (*registry.LocalRegistry, error) { + t.Helper() + self := peer(0x01) + return snapshot(t, self, ®istry.Snapshot{ + DONs: map[registry.DonID]registry.DON{ + 1: { + DON: capabilities.DON{ID: 1, Members: []ragetypes.PeerID{self}}, + CapabilityConfigurations: map[string]registry.CapabilityConfiguration{"vault@1.0.0": {Config: raw}}, + }, + }, + Nodes: map[ragetypes.PeerID]registry.NodeInfo{self: {P2pID: self}}, + Capabilities: map[string]registry.Capability{"vault@1.0.0": {ID: "vault@1.0.0"}}, + }) +} + +func TestServer_ConfigForCapability(t *testing.T) { + ctx := context.Background() + + defaultCfg, err := values.NewMap(map[string]any{"VaultPublicKey": "0xabc"}) + require.NoError(t, err) + restrictedCfg, err := values.NewMap(map[string]any{"OnlyWeSetThis": "0xdef"}) + require.NoError(t, err) + + // Stored as the contract stores it: wire-encoded CapabilityConfig bytes. The + // server parses them and the client decodes, so the on-chain encoding and the + // wire message must agree end to end. + wire, err := proto.Marshal(&capabilitiespb.CapabilityConfig{ + DefaultConfig: values.ProtoMap(defaultCfg), + RestrictedConfig: values.ProtoMap(restrictedCfg), + RestrictedKeys: []string{"OnlyWeSetThis"}, + LocalOnly: true, + }) + require.NoError(t, err) + + c := newClient(t, newRegistryWith(t, nil, configSnapshot(t, wire)), nil) + + got, err := c.ConfigForCapability(ctx, "vault@1.0.0", 1) + require.NoError(t, err) + assert.Equal(t, defaultCfg, got.DefaultConfig) + assert.True(t, got.LocalOnly) + + // RestrictedConfig overrides anything a user supplies, so losing it in transit would silently + // hand the user's own value the authority this one is supposed to have. + assert.Equal(t, restrictedCfg, got.RestrictedConfig) + assert.Equal(t, []string{"OnlyWeSetThis"}, got.RestrictedKeys) +} + +func TestServer_ConfigForCapabilityRejectsGarbageBytes(t *testing.T) { + ctx := context.Background() + + c := newClient(t, newRegistryWith(t, nil, configSnapshot(t, []byte("not a proto"))), nil) + + _, err := c.ConfigForCapability(ctx, "vault@1.0.0", 1) + require.Error(t, err) + assert.Equal(t, codes.Internal, status.Code(err)) +} + +func TestServer_DONsForCapabilityAndDONByID(t *testing.T) { + ctx := context.Background() + + member := peer(0x01) + reg := newRegistryWith(t, nil, snapshot(t, member, ®istry.Snapshot{ + DONs: map[registry.DonID]registry.DON{ + 7: { + DON: capabilities.DON{ + ID: 7, Name: "cap-don", F: 1, + Members: []ragetypes.PeerID{member}, + Families: []string{"zone-b"}, + }, + CapabilityConfigurations: map[string]registry.CapabilityConfiguration{"act@1.0.0": {}}, + }, + }, + Nodes: map[ragetypes.PeerID]registry.NodeInfo{member: {P2pID: member}}, + Capabilities: map[string]registry.Capability{"act@1.0.0": {ID: "act@1.0.0"}}, + })) + c := newClient(t, reg, nil) + + dons, err := c.DONsForCapability(ctx, "act@1.0.0") + require.NoError(t, err) + require.Len(t, dons, 1) + assert.Equal(t, uint32(7), dons[0].DON.ID) + require.Len(t, dons[0].Nodes, 1) + require.NotNil(t, dons[0].Nodes[0].PeerID) + assert.Equal(t, member, *dons[0].Nodes[0].PeerID) + + got, err := c.DONByID(ctx, 7) + require.NoError(t, err) + assert.Equal(t, "cap-don", got.Name) + assert.Equal(t, []string{"zone-b"}, got.Families) +} + +// fakeCombined serves both the executable and trigger surfaces. +type fakeCombined struct { + *fakeExecutable + *fakeTrigger +} + +func (f *fakeCombined) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { + return f.fakeExecutable.Info(ctx) +} + +func Serve(t testing.TB, reg *registry.Registry) *grpc.ClientConn { + t.Helper() + + s := grpc.NewServer() + registrypb.RegisterCapabilitiesRegistryServer(s, registry.NewServer(reg)) + + lis := bufconn.Listen(1 << 20) + go func() { _ = s.Serve(lis) }() + t.Cleanup(s.Stop) + + conn, err := grpc.NewClient("passthrough:///registry", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { + return lis.DialContext(ctx) + }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("failed to dial the test registry: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + return conn +} + +// AddrBook hands out in-memory capability addresses and dials them back, so a +// client's dial-by-address path runs for real without needing ports. +// +// Addresses use the passthrough scheme: gRPC would otherwise parse a bare +// "host:port" as a URI scheme and try to resolve it via DNS. +type AddrBook struct { + mu sync.Mutex + n int + listeners map[string]*bufconn.Listener + dials map[string]int +} + +func NewAddrBook() *AddrBook { + return &AddrBook{ + listeners: map[string]*bufconn.Listener{}, + dials: map[string]int{}, + } +} + +// Listen registers a new listener and returns it. Its Addr is unique per call, +// so a caller that opens one listener per capability announces distinct +// addresses, exactly as it would with port 0. +func (b *AddrBook) Listen() net.Listener { + b.mu.Lock() + defer b.mu.Unlock() + + b.n++ + name := fmt.Sprintf("passthrough:///cap-%d", b.n) + lis := bufconn.Listen(1 << 20) + b.listeners[name] = lis + return &namedListener{Listener: lis, addr: memAddr(name)} +} + +// Target returns the address for an explicitly named listener, registering it. +func (b *AddrBook) Target(name string) string { return "passthrough:///" + name } + +// Serve registers srv under name and starts serving it, returning the address a +// registry should hand out for it. +func (b *AddrBook) Serve(t testing.TB, name string, srv *grpc.Server) string { + t.Helper() + + lis := bufconn.Listen(1 << 20) + target := b.Target(name) + + b.mu.Lock() + b.listeners[target] = lis + b.mu.Unlock() + + go func() { _ = srv.Serve(lis) }() + t.Cleanup(srv.Stop) + + return target +} + +// DialOption resolves the addresses this book handed out. Unknown addresses +// fail, so "the registry returned an address nobody serves" is observable rather +// than a hang. +func (b *AddrBook) DialOption() grpc.DialOption { + return grpc.WithContextDialer(func(ctx context.Context, target string) (net.Conn, error) { + b.mu.Lock() + // The passthrough resolver strips the scheme before dialing, so accept + // either form. + lis, ok := b.listeners[target] + if !ok { + lis, ok = b.listeners["passthrough:///"+target] + } + b.dials[target]++ + b.mu.Unlock() + + if !ok { + return nil, fmt.Errorf("nothing serving %s", target) + } + return lis.DialContext(ctx) + }) +} + +// DialCount reports how many transport dials were made to name, which is how a +// test asserts that connections are reused rather than reopened per call. +func (b *AddrBook) DialCount(name string) int { + b.mu.Lock() + defer b.mu.Unlock() + return b.dials[name] + b.dials["passthrough:///"+name] +} + +type memAddr string + +func (a memAddr) Network() string { return "bufconn" } +func (a memAddr) String() string { return string(a) } + +// namedListener overrides bufconn's fixed Addr so each listener has its own. +type namedListener struct { + net.Listener + addr memAddr +} + +func (l *namedListener) Addr() net.Addr { return l.addr } diff --git a/crecore/registry/syncer.go b/crecore/registry/syncer.go index ac98c06c3..618d3b746 100644 --- a/crecore/registry/syncer.go +++ b/crecore/registry/syncer.go @@ -10,61 +10,85 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" ) // DefaultSyncInterval matches chainlink's registrysyncer tick, so a node fronted // by a standalone process sees registry changes on the same cadence it did before. const DefaultSyncInterval = 12 * time.Second +// ORM persists snapshots so a restart has something to answer from before its first read lands. +type ORM = registrysyncer.ORM + // Syncer keeps a current snapshot of the registry by asking a Reader for one on a timer, and // publishes it to everything that resolves capabilities or their DONs. // // Readers of the snapshot never block on a sync and never see a half-updated view: each sync // replaces the snapshot pointer wholesale, and a lookup takes whichever snapshot was current when // it asked. +// +// Every snapshot read is also stored, and the newest stored one is loaded at startup. The registry +// lives on a chain, and a chain is not always reachable when a process starts; without this, a +// restart would fail every registry lookup until its first read landed, which for a process core +// depends on is a restart that takes the node's capabilities down with it. type Syncer struct { services.StateMachine lggr logger.Logger reader Reader + orm ORM interval time.Duration - // getPeerID answers "which node am I", which a snapshot needs to resolve LocalNode and which - // only this process knows. - getPeerID func() (ragetypes.PeerID, error) - // current holds the most recent successful snapshot, or nil before the first successful sync. + peerID ragetypes.PeerID + + // current holds the most recent snapshot, or nil before the first one lands. current atomic.Pointer[LocalRegistry] stopCh services.StopChan done chan struct{} } -// NewSyncer returns a Syncer that keeps up with reader. getPeerID identifies this process to the -// snapshots it publishes. A non-positive interval means DefaultSyncInterval. -func NewSyncer(lggr logger.Logger, reader Reader, getPeerID func() (ragetypes.PeerID, error), interval time.Duration) (*Syncer, error) { - if reader == nil { - return nil, errors.New("a registry reader is required") - } - if getPeerID == nil { - return nil, errors.New("a peer ID is required to resolve this node in the registry") - } +// NewSyncer returns a Syncer that keeps up with reader, storing what it reads through orm. +// getPeerID identifies this process to the snapshots it publishes. A non-positive interval means +// DefaultSyncInterval. +// +// What it was given is checked when it starts rather than here, so that a Syncer always exists to +// be wired to the things that read from it: whether it can run is a startup question, and startup +// is where an answer of "no" has somewhere to go. +func NewSyncer( + lggr logger.Logger, + reader Reader, + orm ORM, + peerID ragetypes.PeerID, + interval time.Duration, +) *Syncer { if interval <= 0 { interval = DefaultSyncInterval } return &Syncer{ - lggr: logger.Named(lggr, "RegistrySyncer"), - reader: reader, - getPeerID: getPeerID, - interval: interval, - stopCh: make(services.StopChan), - done: make(chan struct{}), - }, nil + lggr: logger.Named(lggr, "RegistrySyncer"), + reader: reader, + orm: orm, + peerID: peerID, + interval: interval, + stopCh: make(services.StopChan), + done: make(chan struct{}), + } } func (s *Syncer) Name() string { return s.lggr.Name() } func (s *Syncer) Start(context.Context) error { return s.StartOnce("RegistrySyncer", func() error { + switch { + case s.reader == nil: + return errors.New("a registry reader is required") + case s.orm == nil: + return errors.New("somewhere to store registry snapshots is required") + case len(s.peerID) == 0: + return errors.New("a peer ID is required to resolve this node in the registry") + } go s.syncLoop() return nil }) @@ -83,7 +107,7 @@ func (s *Syncer) Close() error { func (s *Syncer) HealthReport() map[string]error { err := s.Healthy() if s.current.Load() == nil { - err = errors.New("no successful registry sync yet") + err = errors.New("no registry snapshot yet") } return map[string]error{s.Name(): err} } @@ -106,8 +130,12 @@ func (s *Syncer) syncLoop() { ticker := time.NewTicker(s.interval) defer ticker.Stop() - // Sync once up front: a ticker first fires at T+interval, and every reader of this process - // blocks until the first snapshot exists. + // Publish the stored snapshot first so lookups are answerable while the first read is still in + // flight, and still answerable if it fails outright. + s.restore(ctx) + + // Sync once up front: a ticker first fires at T+interval, and until something is published every + // reader of this process is blocked. if err := s.Sync(ctx); err != nil { s.lggr.Errorw("initial registry sync failed", "err", err) } @@ -124,19 +152,52 @@ func (s *Syncer) syncLoop() { } } -// Sync performs one read and, on success, publishes a new snapshot. A failed sync leaves the -// previous snapshot in place: a registry that cannot be reached right now is better served stale -// than not at all, and HealthReport is what says so. +// restore publishes the newest stored snapshot, if there is one. +// +// Having none is the ordinary state on a first run, so it is not an error here: the sync that +// follows is what this process actually relies on, and this only shortens the window before it +// lands. +func (s *Syncer) restore(ctx context.Context) { + lr, err := s.orm.LatestLocalRegistry(ctx) + if err != nil { + s.lggr.Infow("no stored registry snapshot to start from; waiting for the first read", "err", err) + return + } + + // Neither of these survives being stored, so a restored snapshot cannot resolve anything until + // they are put back. + lr.Logger = s.lggr + + lr.GetPeerID = s.getPeerID + + s.current.Store(lr) + s.lggr.Infow("serving a stored registry snapshot until the first read lands", + "capabilities", len(lr.IDsToCapabilities), "dons", len(lr.IDsToDONs), "nodes", len(lr.IDsToNodes)) +} + +// Sync performs one read and, on success, publishes and stores a new snapshot. A failed sync leaves +// the previous snapshot in place: a registry that cannot be reached right now is better served +// stale than not at all, and HealthReport is what says so. func (s *Syncer) Sync(ctx context.Context) error { snap, err := s.reader.Read(ctx) if err != nil { return err } - lr := NewLocalRegistry(s.lggr, s.getPeerID, snap.DONs, snap.Nodes, snap.Capabilities) + lr := registrysyncer.FromSnapshot(s.lggr, s.getPeerID, snap) s.current.Store(lr) s.lggr.Debugw("registry synced", "capabilities", len(lr.IDsToCapabilities), "dons", len(lr.IDsToDONs), "nodes", len(lr.IDsToNodes)) + + // Storing is what the next restart starts from, not what this process serves, so failing to + // store costs a cold start rather than this sync. + if err := s.orm.AddLocalRegistry(ctx, lr); err != nil { + s.lggr.Errorw("failed to store the registry snapshot", "err", err) + } return nil } + +func (s *Syncer) getPeerID() (ragetypes.PeerID, error) { + return s.peerID, nil +} diff --git a/crecore/registry/syncer_test.go b/crecore/registry/syncer_test.go index ca27c82d5..5be6f35c8 100644 --- a/crecore/registry/syncer_test.go +++ b/crecore/registry/syncer_test.go @@ -14,8 +14,16 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" ) +func peer(b byte) ragetypes.PeerID { + var p ragetypes.PeerID + p[0] = b + return p +} + // fakeReader stands in for whatever the registry is actually written on. type fakeReader struct { mu sync.Mutex @@ -68,26 +76,74 @@ func fakeSnapshot(capabilityID string) *Snapshot { } } -func newTestSyncer(t *testing.T, reader Reader, interval time.Duration) *Syncer { +// memoryORM stands in for the table snapshots are stored in. +type memoryORM struct { + mu sync.Mutex + stored []byte + writes int + // loadErr is what a restore fails with, which is also how "nothing stored yet" arrives. + loadErr error + storeErr error +} + +func newMemoryORM() *memoryORM { + return &memoryORM{loadErr: errors.New("no rows in result set")} +} + +func (m *memoryORM) AddLocalRegistry(_ context.Context, lr *LocalRegistry) error { + if m.storeErr != nil { + return m.storeErr + } + b, err := lr.MarshalJSON() + if err != nil { + return err + } + m.mu.Lock() + defer m.mu.Unlock() + m.stored = b + m.writes++ + m.loadErr = nil + return nil +} + +func (m *memoryORM) LatestLocalRegistry(context.Context) (*LocalRegistry, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.loadErr != nil { + return nil, m.loadErr + } + var lr LocalRegistry + if err := lr.UnmarshalJSON(m.stored); err != nil { + return nil, err + } + return &lr, nil +} + +func (m *memoryORM) count() int { + m.mu.Lock() + defer m.mu.Unlock() + return m.writes +} + +func newTestSyncer(t *testing.T, reader Reader, orm ORM, interval time.Duration) *Syncer { t.Helper() - s, err := NewSyncer(logger.Test(t), reader, func() (ragetypes.PeerID, error) { return peer(1), nil }, interval) - require.NoError(t, err) - return s + return NewSyncer(logger.Test(t), reader, orm, peer(1), interval) } -func TestNewSyncer_RequiresAReaderAndAnIdentity(t *testing.T) { +// What a Syncer was given is checked when it starts, so that one always exists to be wired to the +// things that read from it. +func TestSyncer_StartRequiresAReaderAStoreAndAnIdentity(t *testing.T) { lggr := logger.Test(t) - getPeerID := func() (ragetypes.PeerID, error) { return peer(1), nil } - _, err := NewSyncer(lggr, nil, getPeerID, time.Hour) + err := NewSyncer(lggr, nil, newMemoryORM(), peer(1), time.Hour).Start(t.Context()) require.ErrorContains(t, err, "reader is required") - _, err = NewSyncer(lggr, newFakeReader(), nil, time.Hour) - require.ErrorContains(t, err, "peer ID is required") + err = NewSyncer(lggr, newFakeReader(), nil, peer(1), time.Hour).Start(t.Context()) + require.ErrorContains(t, err, "store registry snapshots is required") } func TestSyncer_CurrentBeforeFirstSync(t *testing.T) { - s := newTestSyncer(t, newFakeReader(), time.Hour) + s := newTestSyncer(t, newFakeReader(), newMemoryORM(), time.Hour) _, err := s.Current() require.ErrorContains(t, err, "not synced") @@ -102,7 +158,7 @@ func TestSyncer_CurrentBeforeFirstSync(t *testing.T) { func TestSyncer_SyncPublishesWhatTheReaderReturned(t *testing.T) { ctx := t.Context() - s := newTestSyncer(t, newFakeReader(), time.Hour) + s := newTestSyncer(t, newFakeReader(), newMemoryORM(), time.Hour) require.NoError(t, s.Sync(ctx)) @@ -123,7 +179,7 @@ func TestSyncer_SyncPublishesWhatTheReaderReturned(t *testing.T) { func TestSyncer_SyncErrorsLeavePreviousSnapshot(t *testing.T) { ctx := t.Context() reader := newFakeReader() - s := newTestSyncer(t, reader, time.Hour) + s := newTestSyncer(t, reader, newMemoryORM(), time.Hour) require.NoError(t, s.Sync(ctx)) reader.fail(errors.New("rpc down")) @@ -138,7 +194,7 @@ func TestSyncer_SyncErrorsLeavePreviousSnapshot(t *testing.T) { func TestSyncer_StartSyncsImmediately(t *testing.T) { reader := newFakeReader() // An interval long enough that anything observed came from the initial sync rather than a tick. - s := newTestSyncer(t, reader, time.Hour) + s := newTestSyncer(t, reader, newMemoryORM(), time.Hour) require.NoError(t, s.Start(t.Context())) t.Cleanup(func() { require.NoError(t, s.Close()) }) @@ -154,7 +210,7 @@ func TestSyncer_StartSyncsImmediately(t *testing.T) { func TestSyncer_CloseStopsReading(t *testing.T) { reader := newFakeReader() - s := newTestSyncer(t, reader, 10*time.Millisecond) + s := newTestSyncer(t, reader, newMemoryORM(), 10*time.Millisecond) require.NoError(t, s.Start(t.Context())) select { @@ -168,3 +224,70 @@ func TestSyncer_CloseStopsReading(t *testing.T) { time.Sleep(50 * time.Millisecond) assert.Equal(t, after, reader.count(), "the loop kept reading after Close") } + +// --- persistence --- + +func TestSyncer_StoresWhatItReads(t *testing.T) { + ctx := t.Context() + orm := newMemoryORM() + s := newTestSyncer(t, newFakeReader(), orm, time.Hour) + + require.NoError(t, s.Sync(ctx)) + assert.Equal(t, 1, orm.count(), "a successful read should have been stored") + + // What was stored is what a restart would answer from, so it has to survive the round trip. + restored, err := orm.LatestLocalRegistry(ctx) + require.NoError(t, err) + assert.Contains(t, restored.IDsToCapabilities, "act@1.0.0") + assert.Contains(t, restored.IDsToDONs, DonID(2)) + assert.Contains(t, restored.IDsToNodes, peer(1)) +} + +// The registry lives on a chain, and a chain is not always reachable when a process starts. Without +// the stored snapshot, a restart would fail every registry lookup until its first read landed. +func TestSyncer_ServesTheStoredSnapshotWhenTheFirstReadFails(t *testing.T) { + ctx := t.Context() + + orm := newMemoryORM() + stored := registrysyncer.FromSnapshot(logger.Test(t), + func() (ragetypes.PeerID, error) { return peer(1), nil }, fakeSnapshot("stored@1.0.0")) + require.NoError(t, orm.AddLocalRegistry(ctx, stored)) + + reader := newFakeReader() + reader.fail(errors.New("rpc down")) + s := newTestSyncer(t, reader, orm, time.Hour) + + require.NoError(t, s.Start(ctx)) + t.Cleanup(func() { require.NoError(t, s.Close()) }) + + require.Eventually(t, func() bool { + _, err := s.Current() + return err == nil + }, time.Second, 5*time.Millisecond, "nothing was published even though a snapshot was stored") + + lr, err := s.Current() + require.NoError(t, err) + assert.Contains(t, lr.IDsToCapabilities, "stored@1.0.0") + + // Neither the logger nor the identity survives being stored, so a restored snapshot can only + // answer this if the syncer put them back. + node, err := lr.LocalNode(ctx) + require.NoError(t, err) + assert.Equal(t, uint32(2), node.WorkflowDON.ID) +} + +// A read that cannot be stored is still a read: the process serves it, and only a restart is worse +// off for it. +func TestSyncer_StoreFailureLeavesTheSyncSuccessful(t *testing.T) { + ctx := t.Context() + + orm := newMemoryORM() + orm.storeErr = errors.New("disk on fire") + s := newTestSyncer(t, newFakeReader(), orm, time.Hour) + + require.NoError(t, s.Sync(ctx)) + + lr, err := s.Current() + require.NoError(t, err) + assert.Contains(t, lr.IDsToCapabilities, "act@1.0.0") +} diff --git a/crecore/registry_service.go b/crecore/registry_service.go index e70ee2521..d04673d33 100644 --- a/crecore/registry_service.go +++ b/crecore/registry_service.go @@ -7,15 +7,11 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "google.golang.org/protobuf/proto" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/capabilities/crecore/registry" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" - regserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/server" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types/core" @@ -42,14 +38,13 @@ type registryService struct { // syncInterval is how often the registry is re-read; see main.go. syncInterval time.Duration - lggr logger.Logger - reader registry.Reader + lggr logger.Logger // peerID is this node's own, from the same identity the rage networking uses, so the node record // this process resolves is the node it fronts. peerID ragetypes.PeerID syncer *registry.Syncer - registry *regserver.Registry + registry *registry.Registry } var _ services.Service = (*registryService)(nil) @@ -58,21 +53,26 @@ func newRegistryService( syncInterval time.Duration, lggr logger.Logger, reader registry.Reader, + orm registry.ORM, peerID ragetypes.PeerID, ) *registryService { + syncer := registry.NewSyncer(lggr, reader, orm, + peerID, syncInterval) + s := ®istryService{ syncInterval: syncInterval, lggr: lggr, - reader: reader, peerID: peerID, - // The Registry exists from construction so it can be attached to the - // proxy's gRPC server before either service starts. Its metadata source is - // installed later, in start. + syncer: syncer, + // Both halves exist from construction so the service can be attached to the proxy's gRPC + // server before either starts, and so the registry has somewhere to read metadata from + // without being told about it a second time later. // // Capabilities registered here are served on loopback by the same-host LOOP // process registering them, so insecure credentials are stated explicitly // rather than defaulted in the client (mirrors creregistry.Select). - registry: regserver.New(lggr, grpc.WithTransportCredentials(insecure.NewCredentials())), + registry: registry.New(lggr, syncer.Current, + grpc.WithTransportCredentials(insecure.NewCredentials())), } s.Service, s.eng = services.Config{ Name: "CapabilitiesRegistry", @@ -83,16 +83,7 @@ func newRegistryService( } func (s *registryService) start(ctx context.Context) error { - getPeerID := func() (ragetypes.PeerID, error) { return s.peerID, nil } - - syncer, err := registry.NewSyncer(s.lggr, s.reader, getPeerID, s.syncInterval) - if err != nil { - return fmt.Errorf("failed to create registry syncer: %w", err) - } - s.syncer = syncer - s.registry.SetMetadata(syncer.Metadata()) - - if err := syncer.Start(ctx); err != nil { + if err := s.syncer.Start(ctx); err != nil { return fmt.Errorf("failed to start registry syncer: %w", err) } @@ -102,86 +93,21 @@ func (s *registryService) start(ctx context.Context) error { } func (s *registryService) close() error { - if s.syncer != nil { - return s.syncer.Close() - } - return nil + return s.syncer.Close() } // Register attaches the CapabilitiesRegistry service to a gRPC server. // -// Safe to call before start: the Registry is built in the constructor, and its -// metadata RPCs return a "not ready" error until start installs the syncer and -// the first sync lands. That decouples this service's startup from the proxy's, -// which must register everything before it Serves. +// Safe to call before start: everything it serves exists from construction, and its metadata RPCs +// return a "not ready" error until the first snapshot lands. That decouples this service's startup +// from the proxy's, which must register everything before it Serves. func (s *registryService) Register(srv *grpc.Server) { - regserver.Register(srv, s.registry) + registry.Register(srv, s.registry) } -// CapabilitiesRegistry returns the core.CapabilitiesRegistry don2don.NewDispatcher takes: real -// values (regserver.Registry.Local(), which Add resolves Handles into) plus this service's -// metadata. Both halves are backed by the same registry a LOOP-registered and a dispatcher-reached -// capability go through identically - one gRPC Add, one real entry, callable either way. +// CapabilitiesRegistry returns the core.CapabilitiesRegistry don2don.NewDispatcher takes. Registry +// implements it directly - the same registry a LOOP-registered and a dispatcher-reached capability +// go through identically, one gRPC Add, one real entry, callable either way. func (s *registryService) CapabilitiesRegistry() core.CapabilitiesRegistry { - return registryAdapter{metadata: s.registry, base: s.registry.Local()} -} - -type registryAdapter struct { - metadata *regserver.Registry - base core.CapabilitiesRegistryBase -} - -func (a registryAdapter) Add(ctx context.Context, c capabilities.BaseCapability) error { - return a.base.Add(ctx, c) -} - -func (a registryAdapter) Remove(ctx context.Context, id string) error { - return a.base.Remove(ctx, id) -} - -func (a registryAdapter) Get(ctx context.Context, id string) (capabilities.BaseCapability, error) { - return a.base.Get(ctx, id) -} - -func (a registryAdapter) GetTrigger(ctx context.Context, id string) (capabilities.TriggerCapability, error) { - return a.base.GetTrigger(ctx, id) -} - -func (a registryAdapter) GetExecutable(ctx context.Context, id string) (capabilities.ExecutableCapability, error) { - return a.base.GetExecutable(ctx, id) -} - -func (a registryAdapter) List(ctx context.Context) ([]capabilities.BaseCapability, error) { - return a.base.List(ctx) -} - -func (a registryAdapter) LocalNode(ctx context.Context) (capabilities.Node, error) { - return a.metadata.LocalNode(ctx) -} - -func (a registryAdapter) NodeByPeerID(ctx context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { - return a.metadata.NodeByPeerID(ctx, peerID) -} - -func (a registryAdapter) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { - return a.metadata.DONsForCapability(ctx, capabilityID) -} - -func (a registryAdapter) DONByID(ctx context.Context, donID uint32) (capabilities.DON, error) { - return a.metadata.DONByID(ctx, donID) -} - -// ConfigForCapability decodes the same wire-encoded config the gRPC service's own -// ConfigForCapability RPC parses, since regserver.Registry only stores the raw bytes. -func (a registryAdapter) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { - raw, err := a.metadata.RawConfigForCapability(ctx, capabilityID, donID) - if err != nil { - return capabilities.CapabilityConfiguration{}, err - } - cfg := &capabilitiespb.CapabilityConfig{} - if err := proto.Unmarshal(raw, cfg); err != nil { - return capabilities.CapabilityConfiguration{}, fmt.Errorf( - "capability %s on DON %d has an unparseable on-chain config: %w", capabilityID, donID, err) - } - return capabilitiespb.CapabilityConfigFromProto(cfg) + return s.registry } diff --git a/crecore/server.go b/crecore/server.go index efa864164..a2ba755c5 100644 --- a/crecore/server.go +++ b/crecore/server.go @@ -1,8 +1,10 @@ package main import ( + "errors" "fmt" "io" + "math" "sync" "github.com/smartcontractkit/libocr/commontypes" @@ -85,7 +87,7 @@ func (s *Server) Connect(stream creproxy.BinaryNetworkEndpointProxy_ConnectServe for { req, err := stream.Recv() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil } return err @@ -95,8 +97,15 @@ func (s *Server) Connect(stream creproxy.BinaryNetworkEndpointProxy_ConnectServe case *creproxy.BinaryNetworkClientRequest_NewEndpoint: return fmt.Errorf("NewEndpointRequest not allowed after initial setup") case *creproxy.BinaryNetworkClientRequest_SendTo: + // The wire carries the oracle ID as a uint32 and an OracleID is a uint8, so a value + // that does not fit is refused rather than converted: converting would silently + // truncate it into some other oracle's ID and send the message there. + to, ok := toOracleID(msg.SendTo.ToOracleId) + if !ok { + return fmt.Errorf("oracle ID %d is out of range", msg.SendTo.ToOracleId) + } s.outboundSizes.record(ctx, len(msg.SendTo.Payload)) - endpoint.SendTo(msg.SendTo.Payload, commontypes.OracleID(msg.SendTo.ToOracleId)) + endpoint.SendTo(msg.SendTo.Payload, to) case *creproxy.BinaryNetworkClientRequest_Broadcast: s.outboundSizes.record(ctx, len(msg.Broadcast)) endpoint.Broadcast(msg.Broadcast) @@ -142,3 +151,14 @@ func (s *Server) handleNewEndpoint(req *creproxy.NewEndpointRequest) (commontype return endpoint, nil } + +// toOracleID narrows a wire oracle ID to libocr's, reporting whether it fits. +// +// Shared by both proxy servers: an OracleID is a uint8, the wire field is a uint32, and nothing +// upstream constrains it, so every conversion has to be able to refuse. +func toOracleID(id uint32) (commontypes.OracleID, bool) { + if id > math.MaxUint8 { + return 0, false + } + return commontypes.OracleID(id), true +} diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index 3c6d2525b..e5463496f 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/go-plugin" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" + "google.golang.org/grpc" "github.com/smartcontractkit/chainlink-common/pkg/beholder" contract "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" @@ -49,6 +50,8 @@ type StandaloneConfig struct { // same collector twice fails, so without the label only the first instance's metrics would // be recorded. MetricsRegisterer prometheus.Registerer + + GRPCServer *grpc.Server } type Bootstrapper struct { diff --git a/libs/standalone/cmd_helpers.go b/libs/standalone/cmd_helpers.go deleted file mode 100644 index 973eee13d..000000000 --- a/libs/standalone/cmd_helpers.go +++ /dev/null @@ -1,27 +0,0 @@ -// Package standalone bootstraps single-binary CRE processes. -// -// Flag/env/config-file binding is handled by chainlink-common's pkg/config/flags: a -// dependency declares a tagged config struct and registers it with -// flags.RegisterCommandFlags(cmd, &cfg, "CRE", "CL") from AddCommands, which binds each field -// as a CLI flag, a viper key, and CRE_*/CL_* env vars, then decodes and validates it (see the -// `validate:"..."` tags) before the command runs. -// -// A binary gets two ways to run, as subcommands of its own root command: -// -// - run: one instance, using the transports and stores it is configured with. What a deployed -// binary does, and what the root command does with no subcommand at all. -// - embed: --instances copies of the same thing inside one process, for running a DON on one -// machine without the setup a DON of real nodes needs. -// -// Embedding is arranged entirely by the dependencies. A service is written once and knows nothing -// about it: everything that cannot be shared between instances is resolved through a -// BootstrapDependency, and each instance is handed the dependency that serves it (ForEmbedding) -// before its services are built. So a listening port becomes the next port along, a database -// becomes a schema of its own, and a peer's identity and transport become a derived key and an -// in-process channel - none of which the service serving on that port, or reading that database, -// has to be told about. -// -// Nothing downstream of that has an instance to reason about: an embedded dependency is already -// specific to its instance when it is built, so neither the services (StandaloneConfig) nor the -// dependencies themselves (CommonConfig) are told which one they belong to. -package standalone diff --git a/libs/standalone/config.go b/libs/standalone/config.go index c13c9cfbc..7f31481ed 100644 --- a/libs/standalone/config.go +++ b/libs/standalone/config.go @@ -95,8 +95,8 @@ func (c PrometheusConfig) disabled() bool { return c.Port < 0 } // OS for one already gets a distinct port, whereas adding to it would name specific low ports // nobody chose. func (c PrometheusConfig) portFor(index int) int { - if c.Port == 0 { - return 0 + if c.Port <= 0 { + return c.Port } return c.Port + index } diff --git a/libs/x/README.md b/libs/x/README.md index d4f7c8ff3..1bfa8e37d 100644 --- a/libs/x/README.md +++ b/libs/x/README.md @@ -13,6 +13,10 @@ build against the same code while crecore takes ownership of it: responses (`executable/`), the same in the other direction for triggers, and the transmission schedule that staggers who is asked when. Was `core/capabilities/remote` (+ `core/capabilities/transmission` and `core/capabilities/validation`). +- **`registrysyncer`** — an in-memory snapshot of a CapabilitiesRegistry, the lookups a metadata API + needs (`LocalNode`, `NodeByPeerID`, `DONsForCapability`, `DONByID`, `ConfigForCapability`), and the + ORM that stores a snapshot so a restart can answer from the last known registry before its first + on-chain read lands. Was `core/services/registrysyncer`'s `LocalRegistry` and `orm.go`. The end state is that crecore owns DON-to-DON outright: a new process talks to another DON by asking crecore, rather than by running a peer and a dispatcher of its own. When core no longer uses any of @@ -31,6 +35,16 @@ changed: - The wire protos keep their proto package (`remote`) and field numbers, so only the Go import path moved. Nothing may generate a second copy: two registrations of `remote.MessageBody` in one binary panic at init. +- `registrysyncer` drops core's hand-written capability-config decoder for a call to + chainlink-common's `capabilitiespb.CapabilityConfigFromProto`, which is the same decoder the + registry's wire path already uses and which states outright that a second one could only drift + from it. `CapabilityConfiguration.Unmarshal` is kept as core's own call sites spell it, so the + change is confined to this package. +- `registrysyncer`'s ORM takes its table name, because the processes that keep a registry do not + share a table, and `AddLocalRegistry` takes a pointer — a `LocalRegistry` carries the mutex + guarding its local-node cache, so core's by-value signature copied a lock. +- `NodeInfo` loses core's `CapabilitiesDONIds` and `HashedCapabilityIDs`: nothing outside + `registrysyncer` ever read either, and the vocabulary is now chainlink-common's. `.golangci.yml` excludes this directory from a few of this repo's rules (including its ban on go-ethereum), so the code can stay as core wrote it. That exclusion goes away with the package. diff --git a/libs/x/registrysyncer/local_registry.go b/libs/x/registrysyncer/local_registry.go new file mode 100644 index 000000000..74aba764c --- /dev/null +++ b/libs/x/registrysyncer/local_registry.go @@ -0,0 +1,267 @@ +// Package registrysyncer is an in-memory snapshot of a CapabilitiesRegistry, plus the lookups a +// metadata API needs: who the DONs are, which nodes belong to them, which capabilities they host, +// and which node this process is. +// +// Where the registry actually lives is not this package's business - a Reader supplies whole +// snapshots from wherever it is written, and the on-chain one lives in chainlink-evm. Keeping a +// snapshot current on a timer is the caller's job too; this package only holds one, answers +// questions about it, and (see orm.go) can persist one so a restart has something to answer from +// before its first read lands. +// +// Was core/services/registrysyncer's LocalRegistry. The exported shape is kept as core had it so +// core can alias this package rather than edit its call sites; what changed is that the 130-line +// hand-rolled capability-config decoder is gone, replaced by a call to chainlink-common's +// capabilitiespb.CapabilityConfigFromProto, which is the same decoder the wire path already uses. +package registrysyncer + +import ( + "context" + "errors" + "fmt" + "sync" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + commonregistry "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone/registry" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// A snapshot answers exactly the metadata half of a CapabilitiesRegistry, which is what lets core +// hand one straight to anything expecting that interface. +var _ core.CapabilitiesRegistryMetadata = (*LocalRegistry)(nil) + +// The registry's own vocabulary comes from the contract a Reader fills in, aliased so this package +// reads as though it owned them. Nothing is redefined here: a snapshot holds exactly what a Reader +// returned, so a second set of types could only be the same types with a conversion between them. +type ( + DonID = commonregistry.DonID + DON = commonregistry.DON + CapabilityConfiguration = commonregistry.CapabilityConfiguration + NodeInfo = commonregistry.NodeInfo + Capability = commonregistry.Capability + Snapshot = commonregistry.Snapshot + Reader = commonregistry.Reader +) + +// LocalRegistry is an immutable snapshot of the on-chain registry, plus the lookups the metadata +// API needs. +// +// A LocalRegistry is never mutated after construction; a caller that keeps polling swaps whole +// snapshots. +// +// Logger and GetPeerID are exported and set after construction on purpose: a snapshot restored from +// the database (see orm.go) cannot carry either across JSON, so whoever restores one puts them +// back. +type LocalRegistry struct { + Logger logger.Logger + GetPeerID func() (ragetypes.PeerID, error) + IDsToDONs map[DonID]DON + IDsToNodes map[ragetypes.PeerID]NodeInfo + IDsToCapabilities map[string]Capability + + cacheMu sync.RWMutex + cachedLocalNodePeer ragetypes.PeerID + cachedLocalNode capabilities.Node +} + +func NewLocalRegistry( + lggr logger.Logger, + getPeerID func() (ragetypes.PeerID, error), + idsToDONs map[DonID]DON, + idsToNodes map[ragetypes.PeerID]NodeInfo, + idsToCapabilities map[string]Capability, +) *LocalRegistry { + return &LocalRegistry{ + Logger: logger.Named(lggr, "LocalRegistry"), + GetPeerID: getPeerID, + IDsToDONs: idsToDONs, + IDsToNodes: idsToNodes, + IDsToCapabilities: idsToCapabilities, + } +} + +// FromSnapshot builds a LocalRegistry from what a Reader returned. +// +// There is nothing to convert: a snapshot holds the contract's own types, so this only supplies the +// two things a Reader has no way to know - who is asking, and where to log. +func FromSnapshot( + lggr logger.Logger, + getPeerID func() (ragetypes.PeerID, error), + snap *Snapshot, +) *LocalRegistry { + return NewLocalRegistry(lggr, getPeerID, snap.DONs, snap.Nodes, snap.Capabilities) +} + +// LocalNode resolves this process's own node record. The result is cached: the +// peer ID cannot change at runtime, so a changed peer ID is logged as a bug +// rather than silently accepted. +func (l *LocalRegistry) LocalNode(ctx context.Context) (capabilities.Node, error) { + pid, err := l.GetPeerID() + if err != nil { + return capabilities.Node{}, fmt.Errorf("unable to get local node peer ID: %w", err) + } + + l.cacheMu.RLock() + if l.cachedLocalNodePeer != (ragetypes.PeerID{}) && l.cachedLocalNodePeer == pid { + node := l.cachedLocalNode + l.cacheMu.RUnlock() + return node, nil + } + l.cacheMu.RUnlock() + + l.cacheMu.Lock() + defer l.cacheMu.Unlock() + if l.cachedLocalNodePeer != (ragetypes.PeerID{}) && l.cachedLocalNodePeer == pid { + return l.cachedLocalNode, nil + } + + if l.cachedLocalNodePeer != (ragetypes.PeerID{}) { + l.Logger.Errorw("node's peerID changed at runtime, this should never happen", + "cachedLocalNodePeer", l.cachedLocalNodePeer, "currentPeerID", pid) + } + + n, err := l.NodeByPeerID(ctx, pid) + if err != nil { + return n, err + } + l.cachedLocalNode = n + l.cachedLocalNodePeer = pid + return n, nil +} + +func (l *LocalRegistry) NodeByPeerID(_ context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { + if err := l.ensureNotEmpty(); err != nil { + return capabilities.Node{}, err + } + nodeInfo, ok := l.IDsToNodes[peerID] + if !ok { + return capabilities.Node{}, errors.New("could not find peerID " + peerID.String()) + } + + var workflowDON capabilities.DON + var capabilityDONs []capabilities.DON + for _, d := range l.IDsToDONs { + for _, p := range d.Members { + if p != peerID { + continue + } + if d.AcceptsWorkflows { + // The CapabilitiesRegistry enforces DON ID > 0, so a zero ID here + // means workflowDON has not been assigned yet. + if workflowDON.ID == 0 { + workflowDON = d.DON + } else { + l.Logger.Errorw("Configuration error: node belongs to more than one workflowDON", "peerID", peerID) + } + } + capabilityDONs = append(capabilityDONs, d.DON) + } + } + + return capabilities.Node{ + PeerID: &peerID, + NodeOperatorID: nodeInfo.NodeOperatorID, + Signer: nodeInfo.Signer, + EncryptionPublicKey: nodeInfo.EncryptionPublicKey, + WorkflowDON: workflowDON, + CapabilityDONs: capabilityDONs, + }, nil +} + +func (l *LocalRegistry) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { + if err := l.ensureNotEmpty(); err != nil { + return nil, err + } + + found := []capabilities.DONWithNodes{} + for _, don := range l.IDsToDONs { + if _, ok := don.CapabilityConfigurations[capabilityID]; !ok { + continue + } + nodes, err := l.nodesForDON(ctx, don.DON) + if err != nil { + return nil, fmt.Errorf("could not fetch nodes for DON %d: %w", don.ID, err) + } + found = append(found, capabilities.DONWithNodes{DON: don.DON, Nodes: nodes}) + } + + if len(found) == 0 { + return nil, fmt.Errorf("could not find DON for capability %s", capabilityID) + } + return found, nil +} + +func (l *LocalRegistry) nodesForDON(ctx context.Context, don capabilities.DON) ([]capabilities.Node, error) { + nodes := make([]capabilities.Node, 0, len(don.Members)) + for _, n := range don.Members { + node, err := l.NodeByPeerID(ctx, n) + if err != nil { + return nil, fmt.Errorf("could not find node for peerID %s: %w", n.String(), err) + } + nodes = append(nodes, node) + } + return nodes, nil +} + +// DONByID resolves any DON known to the registry, including caller/workflow DONs +// that host no capability. Callers need this to read a caller DON's Families +// (e.g. zone membership) from its WorkflowDonID. +func (l *LocalRegistry) DONByID(_ context.Context, donID uint32) (capabilities.DON, error) { + if err := l.ensureNotEmpty(); err != nil { + return capabilities.DON{}, err + } + d, ok := l.IDsToDONs[DonID(donID)] + if !ok { + return capabilities.DON{}, fmt.Errorf("could not find don %d", donID) + } + return d.DON, nil +} + +// ConfigForCapability returns the decoded configuration for a capability on a DON. +func (l *LocalRegistry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { + cfg, err := l.capabilityConfig(capabilityID, donID) + if err != nil { + return capabilities.CapabilityConfiguration{}, err + } + return cfg.Unmarshal() +} + +// RawConfigForCapability returns the same configuration undecoded, for a caller that only passes it +// on - the proxy serves these bytes verbatim rather than decoding and re-encoding them. +func (l *LocalRegistry) RawConfigForCapability(_ context.Context, capabilityID string, donID uint32) ([]byte, error) { + cfg, err := l.capabilityConfig(capabilityID, donID) + if err != nil { + return nil, err + } + return cfg.Config, nil +} + +func (l *LocalRegistry) capabilityConfig(capabilityID string, donID uint32) (CapabilityConfiguration, error) { + if err := l.ensureNotEmpty(); err != nil { + return CapabilityConfiguration{}, err + } + d, ok := l.IDsToDONs[DonID(donID)] + if !ok { + return CapabilityConfiguration{}, fmt.Errorf("could not find don %d", donID) + } + cfg, ok := d.CapabilityConfigurations[capabilityID] + if !ok { + return CapabilityConfiguration{}, fmt.Errorf("could not find capability configuration for capability %s and donID %d", capabilityID, donID) + } + return cfg, nil +} + +func (l *LocalRegistry) ensureNotEmpty() error { + if len(l.IDsToDONs) == 0 { + return errors.New("empty local registry: no DONs registered") + } + if len(l.IDsToNodes) == 0 { + return errors.New("empty local registry: no nodes registered") + } + if len(l.IDsToCapabilities) == 0 { + return errors.New("empty local registry: no capabilities registered") + } + return nil +} diff --git a/crecore/registry/local_registry_test.go b/libs/x/registrysyncer/local_registry_test.go similarity index 96% rename from crecore/registry/local_registry_test.go rename to libs/x/registrysyncer/local_registry_test.go index c9d971efb..f91aa8c9a 100644 --- a/crecore/registry/local_registry_test.go +++ b/libs/x/registrysyncer/local_registry_test.go @@ -1,4 +1,4 @@ -package registry +package registrysyncer import ( "context" @@ -38,14 +38,14 @@ func testLocalRegistry(t *testing.T, self ragetypes.PeerID) *LocalRegistry { Families: []string{"zone-a"}, AcceptsWorkflows: true, }, - CapabilityConfigurations: map[string][]byte{}, + CapabilityConfigurations: map[string]CapabilityConfiguration{}, }, 2: { DON: capabilities.DON{ ID: 2, Name: "cap-don", F: 2, ConfigVersion: 5, Members: []ragetypes.PeerID{p1, p3}, }, - CapabilityConfigurations: map[string][]byte{"act@1.0.0": []byte("cfg-bytes")}, + CapabilityConfigurations: map[string]CapabilityConfiguration{"act@1.0.0": {Config: []byte("cfg-bytes")}}, }, }, map[ragetypes.PeerID]NodeInfo{ @@ -184,9 +184,6 @@ func TestLocalRegistry_RawConfigForCapability(t *testing.T) { _, err = lr.RawConfigForCapability(ctx, "nope@1.0.0", 2) require.Error(t, err) - - _, err = lr.RawConfigForCapability(ctx, "act@1.0.0", 404) - require.Error(t, err) } func TestLocalRegistry_EmptySnapshotIsRejected(t *testing.T) { diff --git a/libs/x/registrysyncer/orm.go b/libs/x/registrysyncer/orm.go new file mode 100644 index 000000000..33e8d9d99 --- /dev/null +++ b/libs/x/registrysyncer/orm.go @@ -0,0 +1,194 @@ +package registrysyncer + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "regexp" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" +) + +// The registry is read from a chain, and a chain is not always reachable when a process starts. A +// snapshot is therefore persisted after every successful read and loaded once at startup, so a +// restart can answer registry questions immediately instead of failing every lookup until its first +// read lands. +// +// What is stored is the snapshot as JSON, keyed by a hash so an unchanged registry does not write a +// new row. Only the ten most recent rows are kept: this is a cache with an on-chain source of +// truth, not a history anyone reads. + +// persistedNodeInfo is the stored form of a NodeInfo. +// +// The JSON keys are pinned here rather than inherited from the Go field names so that renaming a +// field does not silently orphan every row already written. The keys, and the shape of each value, +// are the ones core's registrysyncer already writes, so rows written before this package existed +// still load: Signer and P2pID are ragetypes.PeerID because that is how they were stored, and a +// PeerID marshals as its own string form rather than as 32 numbers. +// +// Fields core did not store (CsaKey, CapabilityIDs) are simply absent from an older row and come +// back zero, which the next sync overwrites. +type persistedNodeInfo struct { + NodeOperatorID uint32 `json:"nodeOperatorId"` + ConfigCount uint32 `json:"configCount"` + WorkflowDONID uint32 `json:"workflowDONId"` + Signer ragetypes.PeerID `json:"signer"` + P2pID ragetypes.PeerID `json:"p2pId"` + EncryptionPublicKey [32]byte `json:"encryptionPublicKey"` + CsaKey [32]byte `json:"csaKey"` + CapabilityIDs []string `json:"capabilityIds"` +} + +type persistedRegistry struct { + IDsToDONs map[DonID]DON `json:"IDsToDONs"` + IDsToNodes map[ragetypes.PeerID]persistedNodeInfo `json:"IDsToNodes"` + IDsToCapabilities map[string]Capability `json:"IDsToCapabilities"` +} + +func (l *LocalRegistry) MarshalJSON() ([]byte, error) { + nodes := make(map[ragetypes.PeerID]persistedNodeInfo, len(l.IDsToNodes)) + for id, n := range l.IDsToNodes { + nodes[id] = persistedNodeInfo{ + NodeOperatorID: n.NodeOperatorID, + ConfigCount: n.ConfigCount, + WorkflowDONID: n.WorkflowDONID, + Signer: ragetypes.PeerID(n.Signer), + P2pID: ragetypes.PeerID(n.P2pID), + EncryptionPublicKey: n.EncryptionPublicKey, + CsaKey: n.CsaKey, + CapabilityIDs: n.CapabilityIDs, + } + } + return json.Marshal(&persistedRegistry{ + IDsToDONs: l.IDsToDONs, + IDsToNodes: nodes, + IDsToCapabilities: l.IDsToCapabilities, + }) +} + +// UnmarshalJSON restores a snapshot. Logger and GetPeerID are not stored - neither survives JSON - +// so a restored snapshot is not usable until the caller puts them back. +func (l *LocalRegistry) UnmarshalJSON(data []byte) error { + stored := persistedRegistry{ + IDsToDONs: map[DonID]DON{}, + IDsToNodes: map[ragetypes.PeerID]persistedNodeInfo{}, + IDsToCapabilities: map[string]Capability{}, + } + if err := json.Unmarshal(data, &stored); err != nil { + return fmt.Errorf("failed to unmarshal registry snapshot: %w", err) + } + + l.IDsToDONs = stored.IDsToDONs + l.IDsToCapabilities = stored.IDsToCapabilities + l.IDsToNodes = make(map[ragetypes.PeerID]NodeInfo, len(stored.IDsToNodes)) + for id, n := range stored.IDsToNodes { + l.IDsToNodes[id] = NodeInfo{ + NodeOperatorID: n.NodeOperatorID, + ConfigCount: n.ConfigCount, + WorkflowDONID: n.WorkflowDONID, + Signer: [32]byte(n.Signer), + P2pID: [32]byte(n.P2pID), + EncryptionPublicKey: n.EncryptionPublicKey, + CsaKey: n.CsaKey, + CapabilityIDs: n.CapabilityIDs, + } + } + return nil +} + +// ORM persists registry snapshots. +type ORM interface { + // AddLocalRegistry stores a snapshot, unless one identical to it is already the latest. + // + // By pointer, unlike core's original: a LocalRegistry carries the mutex guarding its local-node + // cache, so taking one by value copies a lock. + AddLocalRegistry(ctx context.Context, localRegistry *LocalRegistry) error + // LatestLocalRegistry returns the most recently stored snapshot. The returned snapshot has no + // Logger or GetPeerID; see UnmarshalJSON. + LatestLocalRegistry(ctx context.Context) (*LocalRegistry, error) +} + +// safeTableName is what a table this writes to may be called. The name is interpolated into SQL, +// which no amount of parameter binding can do for an identifier, so it is constrained here instead: +// callers pass a constant, and anything that is not a plain identifier is refused outright rather +// than escaped and hoped for. +var safeTableName = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) + +type orm struct { + ds sqlutil.DataSource + lggr logger.Logger + table string +} + +var _ ORM = (*orm)(nil) + +// NewORM returns an ORM storing snapshots in table, which the caller is responsible for having +// migrated into existence. The table is named rather than fixed because the processes that keep a +// registry do not share one: each owns its own table in its own migrations. +// +// Panics if table is not a plain identifier. Callers pass a constant, so a name that fails here is +// a bug in the program rather than a condition it could recover from - the same reason +// regexp.MustCompile panics. +func NewORM(ds sqlutil.DataSource, lggr logger.Logger, table string) ORM { + if !safeTableName.MatchString(table) { + panic(fmt.Sprintf("registrysyncer: invalid snapshot table name %q", table)) + } + return &orm{ds: ds, lggr: logger.Named(lggr, "RegistrySyncerORM"), table: table} +} + +func (o *orm) AddLocalRegistry(ctx context.Context, localRegistry *LocalRegistry) error { + return sqlutil.TransactDataSource(ctx, o.ds, nil, func(tx sqlutil.DataSource) error { + snapshot, err := localRegistry.MarshalJSON() + if err != nil { + return err + } + hash := sha256.Sum256(snapshot) + + // Insert only when the newest row is not already this exact snapshot: the registry changes + // far less often than it is read, so most syncs have nothing new to store. + r, err := tx.ExecContext(ctx, fmt.Sprintf(`INSERT INTO %s (data, data_hash) + SELECT $1, $2 + WHERE $2 NOT IN ( + SELECT data_hash FROM %s + ORDER BY id DESC LIMIT 1 + )`, o.table, o.table), snapshot, hex.EncodeToString(hash[:])) + if err != nil { + return fmt.Errorf("failed to insert into %s: %w", o.table, err) + } + if n, _ := r.RowsAffected(); n == 0 { + o.lggr.Debugw("registry snapshot unchanged, nothing stored", "hash", hex.EncodeToString(hash[:])) + return nil + } + o.lggr.Debugw("stored a new registry snapshot", "hash", hex.EncodeToString(hash[:])) + + _, err = tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s +WHERE data_hash NOT IN ( + SELECT data_hash FROM %s + ORDER BY id DESC + LIMIT 10 +)`, o.table, o.table)) + return err + }) +} + +func (o *orm) LatestLocalRegistry(ctx context.Context) (*LocalRegistry, error) { + var snapshot string + err := o.ds.GetContext(ctx, &snapshot, + fmt.Sprintf(`SELECT data FROM %s ORDER BY id DESC LIMIT 1`, o.table)) + if err != nil { + return nil, err + } + + var localRegistry LocalRegistry + if err := localRegistry.UnmarshalJSON([]byte(snapshot)); err != nil { + return nil, err + } + hash := sha256.Sum256([]byte(snapshot)) + o.lggr.Debugw("restored a registry snapshot", "hash", hex.EncodeToString(hash[:])) + return &localRegistry, nil +} diff --git a/libs/x/registrysyncer/orm_test.go b/libs/x/registrysyncer/orm_test.go new file mode 100644 index 000000000..c2eeb0769 --- /dev/null +++ b/libs/x/registrysyncer/orm_test.go @@ -0,0 +1,85 @@ +package registrysyncer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// A stored snapshot is what a restart answers from until its first read lands, so everything the +// lookups need has to survive the round trip. +func TestLocalRegistry_JSONRoundTrip(t *testing.T) { + original := testLocalRegistry(t, peer(1)) + + encoded, err := original.MarshalJSON() + require.NoError(t, err) + + var restored LocalRegistry + require.NoError(t, restored.UnmarshalJSON(encoded)) + + assert.Equal(t, original.IDsToDONs, restored.IDsToDONs) + assert.Equal(t, original.IDsToNodes, restored.IDsToNodes) + assert.Equal(t, original.IDsToCapabilities, restored.IDsToCapabilities) + + // Capability config is what a caller ultimately asks for, and it is stored as raw bytes, so it + // has to come back byte for byte rather than merely present. + raw, err := restored.RawConfigForCapability(t.Context(), "act@1.0.0", 2) + require.NoError(t, err) + assert.Equal(t, []byte("cfg-bytes"), raw) +} + +// Neither of these survives JSON, so a restored snapshot cannot resolve the local node until the +// caller puts them back - which is the one thing a restore has to remember to do. +func TestLocalRegistry_RestoredSnapshotNeedsItsLoggerAndIdentityBack(t *testing.T) { + encoded, err := testLocalRegistry(t, peer(1)).MarshalJSON() + require.NoError(t, err) + + var restored LocalRegistry + require.NoError(t, restored.UnmarshalJSON(encoded)) + require.Nil(t, restored.GetPeerID) + + restored.Logger = logger.Test(t) + restored.GetPeerID = func() (ragetypes.PeerID, error) { return peer(1), nil } + + node, err := restored.LocalNode(t.Context()) + require.NoError(t, err) + assert.Equal(t, uint32(11), node.NodeOperatorID) + assert.Equal(t, uint32(1), node.WorkflowDON.ID) +} + +func TestNewORM_RejectsATableNameItWouldHaveToInterpolate(t *testing.T) { + // The name goes into the SQL text, where binding cannot protect it, so anything that is not a + // plain identifier is refused rather than escaped. + assert.Panics(t, func() { NewORM(nil, logger.Test(t), "snapshots; DROP TABLE users--") }) + assert.Panics(t, func() { NewORM(nil, logger.Test(t), "") }) + assert.NotPanics(t, func() { NewORM(nil, logger.Test(t), "proxy_registry_snapshots") }) +} + +func TestFromSnapshot_WrapsConfigsWithoutDecodingThem(t *testing.T) { + self := peer(1) + lr := FromSnapshot(logger.Test(t), func() (ragetypes.PeerID, error) { return self, nil }, &Snapshot{ + DONs: map[DonID]DON{ + 2: { + DON: capabilities.DON{ID: 2, Members: []ragetypes.PeerID{self}}, + CapabilityConfigurations: map[string]CapabilityConfiguration{"act@1.0.0": {Config: []byte("not a proto")}}, + }, + }, + Nodes: map[ragetypes.PeerID]NodeInfo{self: {P2pID: self}}, + Capabilities: map[string]Capability{"act@1.0.0": {ID: "act@1.0.0"}}, + }) + + // Undecodable bytes are carried without complaint, because nothing decoded them on the way in. + raw, err := lr.RawConfigForCapability(t.Context(), "act@1.0.0", 2) + require.NoError(t, err) + assert.Equal(t, []byte("not a proto"), raw) + + // They only fail for the caller that actually asks for the decoded form. + _, err = lr.ConfigForCapability(t.Context(), "act@1.0.0", 2) + require.Error(t, err) +} From b68518b730e318d75f0e9a36518c94ac9d25a296 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 13 Aug 2026 14:32:09 -0400 Subject: [PATCH 17/30] Simplify ownership of gRPC and HTTP --- crecore/main.go | 32 ++---- crecore/proxy_service.go | 47 ++------- crecore/registry/server.go | 4 +- crecore/registry_service.go | 28 +++-- libs/standalone/bootstrapper.go | 56 ++++++---- libs/standalone/config.go | 53 +++++----- libs/standalone/config_test.go | 25 ++--- libs/standalone/grpcserver.go | 48 +++++++++ libs/standalone/listener/dependency.go | 108 -------------------- libs/standalone/listener/dependency_test.go | 71 ------------- libs/standalone/webserver.go | 12 +-- 11 files changed, 166 insertions(+), 318 deletions(-) create mode 100644 libs/standalone/grpcserver.go delete mode 100644 libs/standalone/listener/dependency.go delete mode 100644 libs/standalone/listener/dependency_test.go diff --git a/crecore/main.go b/crecore/main.go index 22d35a4c9..6ae42bb40 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -5,7 +5,6 @@ import ( "database/sql" "embed" "log" - "net" "github.com/jmoiron/sqlx" "github.com/spf13/cobra" @@ -13,7 +12,6 @@ import ( "github.com/smartcontractkit/capabilities/crecore/registry" "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/db" - "github.com/smartcontractkit/capabilities/libs/standalone/listener" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" @@ -37,9 +35,6 @@ const ocrDiscovererTable = "proxy_ocr_discoverer_announcements" // lookups before its first on-chain read lands. Must match the CREATE TABLE in migrations/0002_*.sql. const registrySnapshotsTable = "proxy_registry_snapshots" -// defaultListenAddress is where the proxy and registry are served when no address is configured. -const defaultListenAddress = ":50051" - func main() { if err := run(); err != nil { log.Fatal(err) @@ -57,11 +52,11 @@ core can delegate its OCR networking (and, in future, DON-to-DON networking) to this process. The peer's identity is the node's own, loaded from the keystore in the database the two share. -It also serves the CapabilitiesRegistry on that same gRPC address, read directly -from chain with an EVM client (no relayer). Core uses that in place of its own -registrysyncer whenever it is delegating rage networking to this process, which -is why the registry address is required: this process running is what enables the -registry, and core does not start without it. +It also serves the CapabilitiesRegistry on that same gRPC address (--grpc.port), +read directly from chain with an EVM client (no relayer). Core uses that in place +of its own registrysyncer whenever it is delegating rage networking to this +process, which is why --grpc.port must be configured: this process running is +what enables the registry, and core does not start without it. Settings can come from flags, from CRE_/CL_ env vars, or from a --config file; run "docs" to write the full reference to docs/CONFIG.md.`, @@ -85,17 +80,12 @@ run "docs" to write the full reference to docs/CONFIG.md.`, // chainlink-evm's business: this names the dependency and never sees an EVM type, and the // client it needs is a dependency of its own rather than one this binary has to hold. readerDep := evmregistry.Dependency(lggr.Named("CapabilitiesRegistry"), evm.Dependency(lggr.Named("EVM"))) - // Where this process serves, as a dependency rather than a setting the proxy service reads: - // the address is the one thing two instances in one process cannot agree on, and resolving it - // per instance keeps that entirely out of the service. - listenerDep := listener.Dependency("proxy", defaultListenAddress) - return standalone.Run4(bootstrapper, func( + return standalone.Run3(bootstrapper, func( ctx context.Context, scfg *standalone.StandaloneConfig, factories *ocr.RageFactories, reader registry.Reader, - lis net.Listener, database *sql.DB, ) []services.Service { // Where the last known registry is kept. Resolving the database again costs nothing - it is @@ -104,14 +94,14 @@ run "docs" to write the full reference to docs/CONFIG.md.`, regORM := registrysyncer.NewORM(sqlx.NewDb(database, "pgx"), scfg.Logger.Named("registry snapshots"), registrySnapshotsTable) + // Both attach to the bootstrapper's shared gRPC server (scfg.GRPCServer) rather than each + // opening a listener, so core reaches both over the single address the bootstrapper serves. regSvc := newRegistryService(cfg.CapabilitiesRegistrySyncInterval.Duration(), - scfg.Logger.Named("capabilities registry"), reader, regORM, factories.PeerID) - // The registry attaches to the proxy's gRPC server, so core reaches both - // over the single address it already configures. - proxySvc := newProxyService(scfg.Logger.Named("proxy service"), lis, &factories.OCRFactories, regSvc.Register) + scfg.Logger.Named("capabilities registry"), reader, regORM, factories.PeerID, scfg.GRPCServer) + proxySvc := newProxyService(scfg.Logger.Named("proxy service"), scfg.GRPCServer, &factories.OCRFactories) // The dispatcher runs the real DON-to-DON work over the same rage connection, rather than // core running it and this process merely fronting it. dispatcherSvc := newDispatcherService(cfg.Dispatcher, scfg.Logger.Named("dispatcher"), factories, regSvc.CapabilitiesRegistry()) return []services.Service{proxySvc, regSvc, dispatcherSvc} - }, ocrDep, readerDep, listenerDep, dbDep) + }, ocrDep, readerDep, dbDep) } diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index d45ba217d..76d4e6c17 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "net" "google.golang.org/grpc" @@ -17,8 +16,8 @@ import ( ) // Config is the root command's configuration, populated by flags.RegisterCommandFlags (see -// main.go). The libocr peer configuration lives on the ocr bootstrap dependency, and the address -// this process serves on lives on the listener dependency. +// main.go). The libocr peer configuration lives on the ocr bootstrap dependency; the address this +// process serves on is the bootstrapper's shared gRPC server (--grpc.port). type Config struct { // CapabilitiesRegistrySyncInterval is how often the on-chain registry is re-read. CapabilitiesRegistrySyncInterval config.Duration `usage:"how often the on-chain registry is re-read"` @@ -41,19 +40,12 @@ type proxyService struct { eng *services.Engine lggr logger.Logger - // listener is where this process serves. It arrives resolved rather than as an address to open, - // so a process running several instances gives each of them a socket of its own without this - // service knowing that more than one exists. Its lifetime is the bootstrapper's, as the - // factories' is: both outlive this service's own start and close. - listener net.Listener - factories *ocr.OCRFactories - - // registrars attach additional gRPC services to this server before it - // Serves. Used so co-located services (e.g. the CapabilitiesRegistry) share - // one address instead of each opening a listener. - registrars []func(*grpc.Server) - - grpcServer *grpc.Server + // grpcServer is the bootstrapper's shared gRPC server for this instance: this service only + // registers its RPCs on it, and the bootstrapper serves it once every other service (e.g. the + // CapabilitiesRegistry) has registered too, so they share one address instead of each opening a + // listener of their own. + grpcServer grpc.ServiceRegistrar + factories *ocr.OCRFactories } var _ services.Service = (*proxyService)(nil) @@ -61,8 +53,8 @@ var _ services.Service = (*proxyService)(nil) // newProxyService builds the proxy service using the standard // services.Config/Engine pattern, so its lifecycle and health integrate with // the bootstrapper's aggregated health report. -func newProxyService(lggr logger.Logger, listener net.Listener, factories *ocr.OCRFactories, registrars ...func(*grpc.Server)) *proxyService { - s := &proxyService{lggr: lggr, listener: listener, factories: factories, registrars: registrars} +func newProxyService(lggr logger.Logger, grpcServer grpc.ServiceRegistrar, factories *ocr.OCRFactories) *proxyService { + s := &proxyService{lggr: lggr, grpcServer: grpcServer, factories: factories} s.Service, s.eng = services.Config{ Name: "P2PProxy", Start: s.start, @@ -77,26 +69,7 @@ func (s *proxyService) start(context.Context) error { } // The factories back both surfaces over the same rage connection and discoverer. - s.grpcServer = grpc.NewServer() creproxy.RegisterBinaryNetworkEndpointProxyServer(s.grpcServer, NewServer(s.factories.OCR2Endpoint, metrics)) creproxy.RegisterEndpoint2ProxyServer(s.grpcServer, NewEndpoint2Server(s.factories.OCR3_1Endpoint, metrics)) - - for _, register := range s.registrars { - register(s.grpcServer) - } - - // Gracefully stop the gRPC server when the engine cancels this context on - // Close; run the (blocking) Serve in a tracked goroutine so start returns - // promptly, per the services.Engine contract. - s.eng.Go(func(ctx context.Context) { - <-ctx.Done() - s.grpcServer.GracefulStop() - }) - s.eng.Go(func(context.Context) { - s.lggr.Infow("p2p proxy serving", "address", s.listener.Addr().String()) - if err := s.grpcServer.Serve(s.listener); err != nil { - s.eng.Errorw("proxy gRPC server stopped", "err", err) - } - }) return nil } diff --git a/crecore/registry/server.go b/crecore/registry/server.go index b8867fe92..14082fed0 100644 --- a/crecore/registry/server.go +++ b/crecore/registry/server.go @@ -30,8 +30,8 @@ var _ registrypb.CapabilitiesRegistryServer = (*Server)(nil) func NewServer(impl *Registry) *Server { return &Server{impl: impl} } -// Register registers this server on a plain grpc.Server. -func Register(s *grpc.Server, impl *Registry) { +// Register registers this server on a gRPC server. +func Register(s grpc.ServiceRegistrar, impl *Registry) { registrypb.RegisterCapabilitiesRegistryServer(s, NewServer(impl)) } diff --git a/crecore/registry_service.go b/crecore/registry_service.go index d04673d33..618dab81c 100644 --- a/crecore/registry_service.go +++ b/crecore/registry_service.go @@ -27,10 +27,10 @@ import ( // There is no enable switch: this binary running is what enables the registry, // and core does not start without it. // -// It attaches to the proxy's gRPC server rather than opening a listener of its -// own: a node that delegates rage networking to this process already has a -// connection to it, and a second address would be one more thing to configure and -// keep in sync for no gain. +// It attaches to the bootstrapper's shared gRPC server rather than opening a listener of its own: a +// node that delegates rage networking to this process already has a connection to it (the proxy +// service attaches to the same server), and a second address would be one more thing to configure +// and keep in sync for no gain. type registryService struct { services.Service eng *services.Engine @@ -55,6 +55,7 @@ func newRegistryService( reader registry.Reader, orm registry.ORM, peerID ragetypes.PeerID, + grpcServer grpc.ServiceRegistrar, ) *registryService { syncer := registry.NewSyncer(lggr, reader, orm, peerID, syncInterval) @@ -64,9 +65,9 @@ func newRegistryService( lggr: lggr, peerID: peerID, syncer: syncer, - // Both halves exist from construction so the service can be attached to the proxy's gRPC - // server before either starts, and so the registry has somewhere to read metadata from - // without being told about it a second time later. + // Both halves exist from construction so the registry can be registered on grpcServer before + // either starts, and so the registry has somewhere to read metadata from without being told + // about it a second time later. // // Capabilities registered here are served on loopback by the same-host LOOP // process registering them, so insecure credentials are stated explicitly @@ -74,6 +75,10 @@ func newRegistryService( registry: registry.New(lggr, syncer.Current, grpc.WithTransportCredentials(insecure.NewCredentials())), } + // Safe to register before start: everything it serves exists from construction, and its + // metadata RPCs return a "not ready" error until the first snapshot lands. + registry.Register(grpcServer, s.registry) + s.Service, s.eng = services.Config{ Name: "CapabilitiesRegistry", Start: s.start, @@ -96,15 +101,6 @@ func (s *registryService) close() error { return s.syncer.Close() } -// Register attaches the CapabilitiesRegistry service to a gRPC server. -// -// Safe to call before start: everything it serves exists from construction, and its metadata RPCs -// return a "not ready" error until the first snapshot lands. That decouples this service's startup -// from the proxy's, which must register everything before it Serves. -func (s *registryService) Register(srv *grpc.Server) { - registry.Register(srv, s.registry) -} - // CapabilitiesRegistry returns the core.CapabilitiesRegistry don2don.NewDispatcher takes. Registry // implements it directly - the same registry a LOOP-registered and a dispatcher-reached capability // go through identically, one gRPC Add, one real entry, callable either way. diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index e5463496f..bc55fc087 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "io" + "net/http" "os" "os/signal" "reflect" @@ -51,7 +52,12 @@ type StandaloneConfig struct { // be recorded. MetricsRegisterer prometheus.Registerer - GRPCServer *grpc.Server + // GRPCServer and Mux are this instance's shared gRPC and HTTP servers. A service registers its + // RPCs/routes on them during construction (factory, i.e. before startInstance's factory call + // returns); the bootstrapper serves both, once everything else in the instance has started - + // see startGRPCServer and startWebServer. + GRPCServer grpc.ServiceRegistrar + Mux *http.ServeMux } type Bootstrapper struct { @@ -167,10 +173,10 @@ type instantiator func(index int, embed bool) instanceServices // Each subcommand runs instantiate once per instance, composing that instance's services into a // single supervising service via services.Engine sub-services, so their health is aggregated the // same way the rest of the stack does it (services.Config.NewSubServices + HealthReport). It -// starts them along with a health checker (registered against the aggregated root service) and, -// if a prometheus port is configured, that instance's web server serving /metrics, -// /debug/pprof and /healthz + /readyz, then blocks until an interrupt and closes everything in -// reverse. +// starts them along with a health checker (registered against the aggregated root service), that +// instance's shared HTTP server (serving /metrics, /debug/pprof, /healthz + /readyz, and whatever +// routes a service registered) and its shared gRPC server, then blocks until an interrupt and +// closes everything in reverse. func (b *Bootstrapper) run(instantiate instantiator, configured, embedded []contract.BootstrapCommand) error { defer b.close() @@ -197,7 +203,7 @@ Its settings are its own: a dependency that an embedded instance replaces rather Dependencies serve each instance rather than being configured per instance: the transports between instances are replaced by in-process ones, identities that would be read from a database are derived from the instance index instead, and what state instances do keep is partitioned per instance. Each -instance reports its own health, on the configured prometheus port plus its index. +instance reports its own health, on the configured HTTP port plus its index. So the settings here are not "run" settings plus a count: what an embedded instance derives or replaces, it cannot be told, and only what it still needs is accepted.`, @@ -277,7 +283,7 @@ func (b *Bootstrapper) runInstances(ctx context.Context, stop context.CancelFunc // for shutdown. Everything it registers is closed in reverse order by close, which puts an // instance's services ahead of the dependencies they resolved during start. func (b *Bootstrapper) startInstance(ctx context.Context, index, count int, factory instanceServices) error { - cfg := b.instanceConfig(index, count) + cfg, grpcServer := b.instanceConfig(index, count) svcs, err := factory(ctx, cfg) if err != nil { @@ -300,29 +306,43 @@ func (b *Bootstrapper) startInstance(ctx context.Context, index, count int, fact } b.registerCloser(checker) - if b.observability.prometheus.disabled() { - return nil - } - // Instance i takes the configured port plus i, so several instances can serve their own - // health without being configured a port each. - web, err := startWebServer(ctx, cfg.Logger, b.observability.prometheus.portFor(index), checker) + // Everything else has started, so every service has registered what it will on cfg.Mux and + // cfg.GRPCServer by now: only now do the shared servers start actually serving. Instance i takes + // its configured port plus i, so several instances can serve their own without being configured + // a port each. + web, err := startWebServer(ctx, cfg.Logger, b.observability.http.portFor(index), cfg.Mux, checker) if err != nil { return err } b.registerCloser(web) + + grpcSrv, err := startGRPCServer(ctx, cfg.Logger, int(b.observability.grpc.portFor(index)), grpcServer) + if err != nil { + return err + } + b.registerCloser(grpcSrv) + return nil } -// instanceConfig is the StandaloneConfig handed to one instance's factory: the process-wide values, -// plus the logger and registerer that belong to that instance alone. +// instanceConfig builds the StandaloneConfig handed to one instance's factory - the process-wide +// values, the logger and registerer that belong to that instance alone, and a fresh shared HTTP +// mux and gRPC server for services to register on - plus the concrete gRPC server, which +// startInstance needs to serve after everything else has started but which a factory has no +// business being able to Serve or GracefulStop directly (cfg only exposes the narrower +// grpc.ServiceRegistrar). // // It deliberately does not say which instance this is. A service is written once and knows nothing // about embedding; everything that has to differ between instances is a dependency, replaced by its // embedded form before the service is handed it (see BootstrapDependency.ForEmbedding). -func (b *Bootstrapper) instanceConfig(index, count int) *StandaloneConfig { +func (b *Bootstrapper) instanceConfig(index, count int) (*StandaloneConfig, *grpc.Server) { cfg := *b.config + cfg.Mux = http.NewServeMux() + grpcServer := grpc.NewServer() + cfg.GRPCServer = grpcServer + if count == 1 { - return &cfg + return &cfg, grpcServer } cfg.Logger = logger.Sugared(logger.Named(b.config.Logger, "instance."+strconv.Itoa(index))) @@ -331,7 +351,7 @@ func (b *Bootstrapper) instanceConfig(index, count int) *StandaloneConfig { // not see, and registering the same collector a second time is an error. cfg.MetricsRegisterer = prometheus.WrapRegistererWith( prometheus.Labels{"instance": strconv.Itoa(index)}, prometheus.DefaultRegisterer) - return &cfg + return &cfg, grpcServer } // instanceName names an instance's aggregated service. It carries the index when there is more diff --git a/libs/standalone/config.go b/libs/standalone/config.go index 7f31481ed..db0457beb 100644 --- a/libs/standalone/config.go +++ b/libs/standalone/config.go @@ -27,7 +27,8 @@ const ( tracingNamespace = "tracing" chipIngressNamespace = "chip-ingress" pyroscopeNamespace = "pyroscope" - prometheusNamespace = "prometheus" + httpNamespace = "http" + grpcNamespace = "grpc" // Legacy env prefixes for the two map-valued telemetry settings. pkg/config/flags has no // map support, so those are []string of key=value pairs here; a host sets one env var per @@ -78,27 +79,32 @@ type PyroscopeConfig struct { Environment string `usage:"environment tag attached to profiles"` } -// PrometheusConfig is the web server serving /metrics, /debug/pprof and the health endpoints. +// HTTPConfig is the shared HTTP server: /metrics, /debug/pprof, the health endpoints, and whatever +// routes a service registers on StandaloneConfig.Mux during construction - it is not only +// prometheus's, so it is named for the transport it serves rather than for one of its handlers. // -// Every instance serves its own, so under `embed` instance i listens on Port+i and reports that -// instance's health rather than an aggregate. The default keeps the previous behaviour of an -// unset CL_PROMETHEUS_PORT: no server at all. -type PrometheusConfig struct { - Port int `usage:"port serving /metrics, /debug/pprof, /healthz and /readyz; -1 disables the server, 0 asks the OS for an ephemeral port. Instance i of an embed run listens on this port plus i"` +// Every instance serves its own, so under `embed` instance i listens on Port+i. +type HTTPConfig struct { + Port uint16 `usage:"port serving /metrics, /debug/pprof, /healthz, /readyz and any routes a service registers. Instance i of an embed run listens on this port plus i" validate:"required"` } -// disabled reports whether no web server should be started. -func (c PrometheusConfig) disabled() bool { return c.Port < 0 } - // portFor is the port instance i serves on: the configured port plus i, so instances in one -// process do not collide over it. An ephemeral port is left as it is - every instance asking the -// OS for one already gets a distinct port, whereas adding to it would name specific low ports -// nobody chose. -func (c PrometheusConfig) portFor(index int) int { - if c.Port <= 0 { - return c.Port - } - return c.Port + index +// process do not collide over it. +func (c HTTPConfig) portFor(index int) uint16 { + return c.Port + uint16(index) +} + +// GRPCConfig is the shared gRPC server: whatever services register on StandaloneConfig.GRPCServer +// during construction, served on one address per instance instead of each opening its own. +// +// Every instance serves its own, so under `embed` instance i listens on Port+i. +type GRPCConfig struct { + Port uint16 `usage:"port serving the shared gRPC server. Instance i of an embed run listens on this port plus i" validate:"required"` +} + +// portFor is the port instance i serves on; see HTTPConfig.portFor. +func (c GRPCConfig) portFor(index int) uint16 { + return c.Port + uint16(index) } // observability is every process-wide observability config, registered together by @@ -108,16 +114,16 @@ type observability struct { tracing TracingConfig chipIngress ChipIngressConfig pyroscope PyroscopeConfig - prometheus PrometheusConfig + http HTTPConfig + grpc GRPCConfig } // defaultObservability is what the flags are bound to and decoded into, so an unset setting keeps -// the value it is given here. +// the value it is given here. HTTP and gRPC have no default: both are `validate:"required"`, +// so leaving either unconfigured fails at startup rather than silently picking a port. func defaultObservability() observability { return observability{ tracing: TracingConfig{SamplingRatio: 1}, - // Off unless configured, as an unset CL_PROMETHEUS_PORT was before this was a flag. - prometheus: PrometheusConfig{Port: -1}, } } @@ -135,7 +141,8 @@ func (o *observability) namespaced() []struct { {tracingNamespace, &o.tracing}, {chipIngressNamespace, &o.chipIngress}, {pyroscopeNamespace, &o.pyroscope}, - {prometheusNamespace, &o.prometheus}, + {httpNamespace, &o.http}, + {grpcNamespace, &o.grpc}, } } diff --git a/libs/standalone/config_test.go b/libs/standalone/config_test.go index 423738d12..e3589533b 100644 --- a/libs/standalone/config_test.go +++ b/libs/standalone/config_test.go @@ -7,23 +7,16 @@ import ( "github.com/stretchr/testify/require" ) -func TestPrometheusConfigPortFor(t *testing.T) { - cfg := PrometheusConfig{Port: 9090} - assert.Equal(t, 9090, cfg.portFor(0)) - assert.Equal(t, 9092, cfg.portFor(2)) - assert.False(t, cfg.disabled()) - - t.Run("an ephemeral port stays ephemeral for every instance", func(t *testing.T) { - // Offsetting 0 would turn "any free port" into port 3, which nobody asked for and which an - // unprivileged process may not even be able to bind. - ephemeral := PrometheusConfig{Port: 0} - assert.Equal(t, 0, ephemeral.portFor(3)) - assert.False(t, ephemeral.disabled()) - }) +func TestHTTPConfigPortFor(t *testing.T) { + cfg := HTTPConfig{Port: 9090} + assert.Equal(t, uint16(9090), cfg.portFor(0)) + assert.Equal(t, uint16(9092), cfg.portFor(2)) +} - t.Run("the default leaves the server off, as an unset CL_PROMETHEUS_PORT did", func(t *testing.T) { - assert.True(t, defaultObservability().prometheus.disabled()) - }) +func TestGRPCConfigPortFor(t *testing.T) { + cfg := GRPCConfig{Port: 50051} + assert.Equal(t, uint16(50051), cfg.portFor(0)) + assert.Equal(t, uint16(50053), cfg.portFor(2)) } func TestParsePairs(t *testing.T) { diff --git a/libs/standalone/grpcserver.go b/libs/standalone/grpcserver.go new file mode 100644 index 000000000..96fba29b1 --- /dev/null +++ b/libs/standalone/grpcserver.go @@ -0,0 +1,48 @@ +package standalone + +import ( + "context" + "errors" + "fmt" + "net" + + "google.golang.org/grpc" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// grpcServer serves one instance's shared gRPC server: whatever services registered on +// StandaloneConfig.GRPCServer during construction. +type grpcServer struct { + lggr logger.Logger + server *grpc.Server + listener net.Listener +} + +// startGRPCServer listens on port and serves server, which already has every service's RPCs +// registered on it from earlier in that instance's construction. Port 0 asks the OS for an +// ephemeral port, which is logged once bound. +func startGRPCServer(ctx context.Context, lggr logger.Logger, port int, server *grpc.Server) (*grpcServer, error) { + // An explicit listener resolves port 0 before Serve, so the chosen port can be logged. + var lc net.ListenConfig + listener, err := lc.Listen(ctx, "tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return nil, fmt.Errorf("failed to listen on port %d: %w", port, err) + } + + g := &grpcServer{lggr: lggr, server: server, listener: listener} + + go func() { + if err := server.Serve(listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) { + g.lggr.Errorw("gRPC server stopped", "err", err) + } + }() + + lggr.Infow("Serving gRPC", "address", listener.Addr().String()) + return g, nil +} + +func (g *grpcServer) Close() error { + g.server.GracefulStop() + return nil +} diff --git a/libs/standalone/listener/dependency.go b/libs/standalone/listener/dependency.go deleted file mode 100644 index bec44c8f7..000000000 --- a/libs/standalone/listener/dependency.go +++ /dev/null @@ -1,108 +0,0 @@ -// Package listener provides a standalone.BootstrapDependency that resolves the net.Listener a -// server serves on. -// -// A listening port is the one thing several instances of a binary in one process cannot share, and -// this is where that is dealt with: instance i listens one port along, so a process running an -// embedded DON needs no per-instance configuration and the service that serves on the listener -// never learns which instance it belongs to. That is the point of resolving it as a dependency -// rather than having a service open a port from an address it was configured with - a service -// aware of its own instance index would have to be written twice, once for the single-instance -// case and once for the embedded one. -package listener - -import ( - "context" - "errors" - "fmt" - "net" - "sync" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" -) - -// Config is where the server listens. -type Config struct { - ListenAddress string `usage:"address (host:port) this server listens on; instance i of an embed run listens on the port plus i"` -} - -// Dependency returns a standalone.BootstrapDependency that resolves a listening TCP socket. -// -// namespace groups the setting, so a binary serving on more than one address gives each its own -// ("proxy" yields --proxy.listen-address). defaultAddress is what it listens on when nothing is -// configured; pass "" to require the address to be set. -func Dependency(namespace, defaultAddress string) standalone.BootstrapDependency[net.Listener] { - // Wrap in OnceBootstrapper so the port is bound at most once even if several services resolve - // this dependency - two listeners on one address is an error, and the second service would be - // the one to fail. - return standalone.OnceBootstrapper[net.Listener](&dependency{ - namespace: namespace, - cfg: &Config{ListenAddress: defaultAddress}, - }) -} - -type dependency struct { - namespace string - // cfg is shared with the embedded forms rather than copied into them: it is the instance the - // flags are bound to, so a copy taken before decoding would never see the configured address. - cfg *Config - // portOffset is added to the configured port, set by ForEmbedding. Zero for a single instance, - // which listens exactly where it is configured to. - portOffset int -} - -var _ standalone.BootstrapDependency[net.Listener] = (*dependency)(nil) - -func (d *dependency) Namespace() string { return d.namespace } - -func (d *dependency) Config() any { return d.cfg } - -func (d *dependency) Dependencies() []standalone.BootstrapCommand { - return []standalone.BootstrapCommand{} -} - -// ForEmbedding returns a form that listens i ports along from the configured address, since a port -// is the one thing two instances in one process cannot share. It reads the same settings as the -// receiver - an address, wherever it came from - so those stay one shared config instance, -// registered once and inherited by both commands. -func (d *dependency) ForEmbedding(i int) standalone.BootstrapDependency[net.Listener] { - clone := *d - clone.portOffset = i - return &clone -} - -func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (net.Listener, error) { - if d.cfg.ListenAddress == "" { - return nil, fmt.Errorf("--%s.listen-address is required", d.namespace) - } - - address, err := standalone.OffsetPort(d.cfg.ListenAddress, d.portOffset) - if err != nil { - return nil, err - } - - // Bound through a ListenConfig so the context governs it, as it does every other resolution. - var lc net.ListenConfig - lis, err := lc.Listen(ctx, "tcp", address) - if err != nil { - return nil, fmt.Errorf("failed to listen on %s: %w", address, err) - } - return &listener{Listener: lis}, nil -} - -// listener makes Close idempotent. The bootstrapper closes every resolved dependency on shutdown, -// and a server given a listener closes it as part of stopping, so whichever gets there second -// would otherwise report an already-closed socket as a shutdown failure. -type listener struct { - net.Listener - once sync.Once - err error -} - -func (l *listener) Close() error { - l.once.Do(func() { - if err := l.Listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { - l.err = err - } - }) - return l.err -} diff --git a/libs/standalone/listener/dependency_test.go b/libs/standalone/listener/dependency_test.go deleted file mode 100644 index e679d091d..000000000 --- a/libs/standalone/listener/dependency_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package listener - -import ( - "net" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" -) - -func TestDependency(t *testing.T) { - t.Run("a single-instance run listens on the configured address", func(t *testing.T) { - // Port 0 so the test binds something it is guaranteed to be able to. - dep := Dependency("proxy", "127.0.0.1:0") - - lis, err := dep.Get(t.Context(), standalone.CommonConfig{}) - require.NoError(t, err) - t.Cleanup(func() { assert.NoError(t, lis.Close()) }) - - assert.Equal(t, "127.0.0.1", lis.Addr().(*net.TCPAddr).IP.String()) - }) - - t.Run("each instance listens one port along", func(t *testing.T) { - // Bind an ephemeral port first, then build addresses relative to it, so the ports this - // test needs are ones the OS has just told us are free. - base, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0") - require.NoError(t, err) - require.NoError(t, base.Close()) - basePort := base.Addr().(*net.TCPAddr).Port - - template := Dependency("proxy", net.JoinHostPort("127.0.0.1", strconv.Itoa(basePort))) - - for i := range 2 { - lis, err := template.ForEmbedding(i).Get(t.Context(), standalone.CommonConfig{}) - require.NoError(t, err) - t.Cleanup(func() { assert.NoError(t, lis.Close()) }) - - assert.Equal(t, basePort+i, lis.Addr().(*net.TCPAddr).Port) - } - }) - - t.Run("an unset address is an error naming the flag", func(t *testing.T) { - _, err := Dependency("proxy", "").Get(t.Context(), standalone.CommonConfig{}) - require.ErrorContains(t, err, "--proxy.listen-address is required") - }) - - t.Run("closing twice is not a failure", func(t *testing.T) { - lis, err := Dependency("proxy", "127.0.0.1:0").Get(t.Context(), standalone.CommonConfig{}) - require.NoError(t, err) - - // The bootstrapper closes every resolved dependency, and a server handed a listener closes - // it while stopping; whichever is second must not report a shutdown failure. - require.NoError(t, lis.Close()) - require.NoError(t, lis.Close()) - }) - - t.Run("the same dependency binds one port however many services resolve it", func(t *testing.T) { - dep := Dependency("proxy", "127.0.0.1:0") - - first, err := dep.Get(t.Context(), standalone.CommonConfig{}) - require.NoError(t, err) - t.Cleanup(func() { assert.NoError(t, first.Close()) }) - - second, err := dep.Get(t.Context(), standalone.CommonConfig{}) - require.NoError(t, err) - assert.Same(t, first, second) - }) -} diff --git a/libs/standalone/webserver.go b/libs/standalone/webserver.go index 5869f75df..115c4334a 100644 --- a/libs/standalone/webserver.go +++ b/libs/standalone/webserver.go @@ -16,7 +16,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" ) -// webServer serves one instance's /metrics, /debug/pprof and health endpoints. +// webServer serves one instance's shared mux: /metrics, /debug/pprof, health endpoints, and +// whatever routes a service registered on it during construction. // // Each instance gets its own mux and listener rather than sharing net/http's DefaultServeMux (as // loop.WebServerOpts does): the mux panics on a second registration of the same pattern, so a @@ -29,11 +30,10 @@ type webServer struct { listener net.Listener } -// startWebServer listens on port and serves the endpoints reporting checker's view of one -// instance. Port 0 asks the OS for an ephemeral port, which is logged once bound. -func startWebServer(ctx context.Context, lggr logger.Logger, port int, checker *services.HealthChecker) (*webServer, error) { - mux := http.NewServeMux() - +// startWebServer registers /metrics, /debug/pprof and the health endpoints reporting checker's +// view of one instance onto mux, then listens on port and serves it - along with anything already +// registered on mux by a service built earlier in that instance's construction. +func startWebServer(ctx context.Context, lggr logger.Logger, port uint16, mux *http.ServeMux, checker *services.HealthChecker) (*webServer, error) { mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ EnableOpenMetrics: true, })) From 28435a40f2841dd72b1771bf0f534738125dc868 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Fri, 14 Aug 2026 13:14:47 -0400 Subject: [PATCH 18/30] Start cap helper and move reg stuff to make core mod tidy work --- crecore/go.mod | 6 +- crecore/main.go | 2 +- crecore/proxy_service.go | 2 +- crecore/registry_service.go | 2 +- libs/standalone/bootstrap_gen.go | 152 +++++----- libs/standalone/capability/capability.go | 14 + libs/standalone/capability/dependency.go | 269 ++++++++++++++++++ libs/standalone/capability/embed.go | 76 +++++ libs/standalone/capability/registry.go | 198 +++++++++++++ libs/standalone/capability/settings.go | 68 +++++ libs/standalone/gen/bootstrap.go.tmpl | 8 +- .../x}/registry/local_registry.go | 0 {crecore => libs/x}/registry/registry.go | 0 libs/x/registry/registrytest/registrytest.go | 139 +++++++++ {crecore => libs/x}/registry/server.go | 0 {crecore => libs/x}/registry/server_test.go | 154 +--------- {crecore => libs/x}/registry/syncer.go | 0 {crecore => libs/x}/registry/syncer_test.go | 0 18 files changed, 865 insertions(+), 225 deletions(-) create mode 100644 libs/standalone/capability/capability.go create mode 100644 libs/standalone/capability/dependency.go create mode 100644 libs/standalone/capability/embed.go create mode 100644 libs/standalone/capability/registry.go create mode 100644 libs/standalone/capability/settings.go rename {crecore => libs/x}/registry/local_registry.go (100%) rename {crecore => libs/x}/registry/registry.go (100%) create mode 100644 libs/x/registry/registrytest/registrytest.go rename {crecore => libs/x}/registry/server.go (100%) rename {crecore => libs/x}/registry/server_test.go (82%) rename {crecore => libs/x}/registry/syncer.go (100%) rename {crecore => libs/x}/registry/syncer_test.go (100%) diff --git a/crecore/go.mod b/crecore/go.mod index 0f7551f90..3b46172f2 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -7,16 +7,13 @@ require ( github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 google.golang.org/grpc v1.82.1 - google.golang.org/protobuf v1.36.11 ) require ( @@ -210,6 +207,7 @@ require ( github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect @@ -225,6 +223,7 @@ require ( github.com/stellar/go-stellar-sdk v0.4.0 // indirect github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect @@ -283,6 +282,7 @@ require ( google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/guregu/null.v4 v4.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/crecore/main.go b/crecore/main.go index 6ae42bb40..aa459c6c0 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -9,10 +9,10 @@ import ( "github.com/jmoiron/sqlx" "github.com/spf13/cobra" - "github.com/smartcontractkit/capabilities/crecore/registry" "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/db" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/capabilities/libs/x/registry" "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" "github.com/smartcontractkit/chainlink-evm/pkg/cre/evm" diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index 76d4e6c17..b507381c8 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -6,8 +6,8 @@ import ( "google.golang.org/grpc" - "github.com/smartcontractkit/capabilities/crecore/registry" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/capabilities/libs/x/registry" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" diff --git a/crecore/registry_service.go b/crecore/registry_service.go index 618dab81c..d185b0ad1 100644 --- a/crecore/registry_service.go +++ b/crecore/registry_service.go @@ -10,7 +10,7 @@ import ( ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - "github.com/smartcontractkit/capabilities/crecore/registry" + "github.com/smartcontractkit/capabilities/libs/x/registry" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" diff --git a/libs/standalone/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go index 46841c4ec..300c0f68a 100644 --- a/libs/standalone/bootstrap_gen.go +++ b/libs/standalone/bootstrap_gen.go @@ -6,7 +6,7 @@ import ( "context" "fmt" - contract "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" "github.com/smartcontractkit/chainlink-common/pkg/services" ) @@ -26,7 +26,7 @@ import ( func Run1[T0 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0) []services.Service, - bootDep0 contract.BootstrapDependency[T0], + bootDep0 common.BootstrapDependency[T0], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -41,11 +41,11 @@ func Run1[T0 any]( return fn(ctx, cfg, value0), nil } }, - []contract.BootstrapCommand{bootDep0}, + []common.BootstrapCommand{bootDep0}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0)}, ) } @@ -65,8 +65,8 @@ func Run1[T0 any]( func Run2[T0 any, T1 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -88,11 +88,11 @@ func Run2[T0 any, T1 any]( return fn(ctx, cfg, value0, value1), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1}, + []common.BootstrapCommand{bootDep0, bootDep1}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0)}, ) } @@ -112,9 +112,9 @@ func Run2[T0 any, T1 any]( func Run3[T0 any, T1 any, T2 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -143,11 +143,11 @@ func Run3[T0 any, T1 any, T2 any]( return fn(ctx, cfg, value0, value1, value2), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0)}, ) } @@ -167,10 +167,10 @@ func Run3[T0 any, T1 any, T2 any]( func Run4[T0 any, T1 any, T2 any, T3 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], - bootDep3 contract.BootstrapDependency[T3], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -206,11 +206,11 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( return fn(ctx, cfg, value0, value1, value2, value3), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0)}, ) } @@ -230,11 +230,11 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], - bootDep3 contract.BootstrapDependency[T3], - bootDep4 contract.BootstrapDependency[T4], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], + bootDep4 common.BootstrapDependency[T4], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -277,11 +277,11 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0)}, ) } @@ -301,12 +301,12 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], - bootDep3 contract.BootstrapDependency[T3], - bootDep4 contract.BootstrapDependency[T4], - bootDep5 contract.BootstrapDependency[T5], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], + bootDep4 common.BootstrapDependency[T4], + bootDep5 common.BootstrapDependency[T5], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -356,11 +356,11 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4, value5), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0)}, ) } @@ -380,13 +380,13 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], - bootDep3 contract.BootstrapDependency[T3], - bootDep4 contract.BootstrapDependency[T4], - bootDep5 contract.BootstrapDependency[T5], - bootDep6 contract.BootstrapDependency[T6], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], + bootDep4 common.BootstrapDependency[T4], + bootDep5 common.BootstrapDependency[T5], + bootDep6 common.BootstrapDependency[T6], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -443,11 +443,11 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0)}, ) } @@ -467,14 +467,14 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], - bootDep3 contract.BootstrapDependency[T3], - bootDep4 contract.BootstrapDependency[T4], - bootDep5 contract.BootstrapDependency[T5], - bootDep6 contract.BootstrapDependency[T6], - bootDep7 contract.BootstrapDependency[T7], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], + bootDep4 common.BootstrapDependency[T4], + bootDep5 common.BootstrapDependency[T5], + bootDep6 common.BootstrapDependency[T6], + bootDep7 common.BootstrapDependency[T7], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -538,11 +538,11 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0)}, ) } @@ -562,15 +562,15 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], - bootDep3 contract.BootstrapDependency[T3], - bootDep4 contract.BootstrapDependency[T4], - bootDep5 contract.BootstrapDependency[T5], - bootDep6 contract.BootstrapDependency[T6], - bootDep7 contract.BootstrapDependency[T7], - bootDep8 contract.BootstrapDependency[T8], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], + bootDep4 common.BootstrapDependency[T4], + bootDep5 common.BootstrapDependency[T5], + bootDep6 common.BootstrapDependency[T6], + bootDep7 common.BootstrapDependency[T7], + bootDep8 common.BootstrapDependency[T8], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -641,11 +641,11 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7, value8), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0)}, ) } @@ -665,16 +665,16 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8, dep9 T9) []services.Service, - bootDep0 contract.BootstrapDependency[T0], - bootDep1 contract.BootstrapDependency[T1], - bootDep2 contract.BootstrapDependency[T2], - bootDep3 contract.BootstrapDependency[T3], - bootDep4 contract.BootstrapDependency[T4], - bootDep5 contract.BootstrapDependency[T5], - bootDep6 contract.BootstrapDependency[T6], - bootDep7 contract.BootstrapDependency[T7], - bootDep8 contract.BootstrapDependency[T8], - bootDep9 contract.BootstrapDependency[T9], + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], + bootDep4 common.BootstrapDependency[T4], + bootDep5 common.BootstrapDependency[T5], + bootDep6 common.BootstrapDependency[T6], + bootDep7 common.BootstrapDependency[T7], + bootDep8 common.BootstrapDependency[T8], + bootDep9 common.BootstrapDependency[T9], ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -752,10 +752,10 @@ func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 an return fn(ctx, cfg, value0, value1, value2, value3, value4, value5, value6, value7, value8, value9), nil } }, - []contract.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9}, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9}, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0), bootDep9.ForEmbedding(0)}, + []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0), bootDep9.ForEmbedding(0)}, ) } diff --git a/libs/standalone/capability/capability.go b/libs/standalone/capability/capability.go new file mode 100644 index 000000000..48350bd61 --- /dev/null +++ b/libs/standalone/capability/capability.go @@ -0,0 +1,14 @@ +package capability + +import ( + "context" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +type Capability interface { + services.Service + Initialise(ctx context.Context, dependencies *Dependencies) error + Info(ctx context.Context) (capabilities.CapabilityInfo, error) +} diff --git a/libs/standalone/capability/dependency.go b/libs/standalone/capability/dependency.go new file mode 100644 index 000000000..d85812651 --- /dev/null +++ b/libs/standalone/capability/dependency.go @@ -0,0 +1,269 @@ +package capability + +import ( + "context" + "errors" + "fmt" + "net/http" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/smartcontractkit/capabilities/libs/standalone" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" + registryclient "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" + common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +type Dependencies struct { + LimitsFactory limits.Factory + CRESettings core.SettingsBroadcaster + CapabilityRegistry core.CapabilitiesRegistry + + // CapabilityDonID is the on-chain DON ID of the capability DON this plugin + // process was spawned for, resolved authoritatively by the host before + // Initialise is called. Plugins should use this as the source of truth for + // their own DON identity (e.g. when emitting events that need to carry the + // *sending* DON ID, distinct from the consumer workflow's DON ID). + // + // Zero means the host did not provide one — either a legacy core node that + // pre-dates this field, or a boot path that has not yet been updated to + // populate it. Plugins SHOULD fall back to resolving via the capability + // registry in that case, but the fallback path cannot disambiguate when + // the local node belongs to multiple DONs running the same capability. + CapabilityDonID uint32 + + lggr logger.Logger + + // settings backs CRESettings. Held as the concrete type because Run needs to + // write to it - the reload endpoint's whole job - which the broadcaster + // interface cannot express. + settings *loop.AtomicSettings + // settingsPath is the file the reload endpoint re-reads. + settingsPath string + // closers are the connections Get opened, torn down by Close. + closers []func() error +} + +// Close releases whatever Get dialled. The bootstrapper closes resolved +// dependency values on shutdown, after the services built from them. +func (d Dependencies) Close() error { + var errs []error + for _, c := range d.closers { + errs = append(errs, c()) + } + return errors.Join(errs...) +} + +// capabilityConfig is the configured form's settings, under capabilities.*. +type capabilityConfig struct { + // ProxyURL is a grpc.NewClient target rather than a port, so the registry + // proxy is not assumed to be a process on this machine. The node usually + // runs it beside this one ("localhost:9000"), but a shared proxy, a + // sidecar on another host or a DNS name behind several of them are all just + // a different target - and none of them can be expressed as a port. + ProxyURL string `validate:"required" usage:"gRPC target of the node's capability registry proxy (e.g. localhost:9000, or dns:///registry.internal:9000), used to resolve capabilities this binary does not host"` + CapabilityDonID uint32 `validate:"required" usage:"on-chain DON ID of the capability DON this process was spawned for"` +} + +// Dependency returns the standalone.BootstrapDependency a capability binary +// resolves to get everything a capability needs from its host: the limits +// factory, the CRE settings behind it, and a capability registry. +// +// The three arrive together because they are one thing seen from three sides. +// Settings are what the node broadcasts, limits are those settings resolved per +// key, and the registry is how a capability reaches the ones it does not host. +// Resolving them separately would mean three dependencies agreeing on one +// settings file and one proxy address. +func Dependency(lggr logger.Logger) common.BootstrapDependency[Dependencies] { + // Wrapped so the connections Get dials are made at most once however many + // services resolve this. + return common.OnceBootstrapper[Dependencies](&dependency{lggr: lggr}) +} + +type dependency struct { + lggr logger.Logger + capabilityConfig +} + +var _ common.BootstrapDependency[Dependencies] = (*dependency)(nil) + +func (d *dependency) Namespace() string { return "capabilities" } + +func (d *dependency) Config() any { return &d.capabilityConfig } + +func (d *dependency) Dependencies() []common.BootstrapCommand { return nil } + +// ForEmbedding returns the form with no proxy behind it: an embedded instance +// has no node to ask, so its registry holds only what this binary registers. See +// embedded. +// +// The index is not used: what this dependency resolves belongs to the process +// rather than to an instance, so every instance shares the one form (and, through +// OnceBootstrapper, the one value it resolves to). +func (d *dependency) ForEmbedding(int) common.BootstrapDependency[Dependencies] { + return &embedded{lggr: d.lggr, cfg: embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}} +} + +func (d *dependency) Get(ctx context.Context, _ common.CommonConfig) (Dependencies, error) { + settings, err := newSettings(d.lggr) + if err != nil { + return Dependencies{}, err + } + + // grpc.NewClient does not connect here: the first RPC does. So a proxy that + // is not up yet delays the first lookup rather than failing the whole boot, + // which matters because the node starts this process and the two race. It + // also means an unreachable target is reported where it is used rather than + // here, so the error below is only ever a malformed one. + conn, err := grpc.NewClient(d.ProxyURL, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return Dependencies{}, fmt.Errorf("failed to create registry proxy client for %s: %w", d.ProxyURL, err) + } + proxy := registryclient.New(d.lggr, conn) + + return Dependencies{ + LimitsFactory: newLimitsFactory(d.lggr, settings), + CRESettings: settings, + CapabilityRegistry: newOverlayRegistry(d.lggr, registry.NewBaseRegistry(d.lggr), proxy), + CapabilityDonID: d.CapabilityDonID, + lggr: d.lggr, + settings: settings, + settingsPath: SettingsPath(), + closers: []func() error{proxy.Close, conn.Close}, + }, nil +} + +// newSettings builds the process's settings, seeded from the dumped file if the +// node has already written one. +func newSettings(lggr logger.Logger) (*loop.AtomicSettings, error) { + s := &loop.AtomicSettings{Lggr: lggr} + s.SetGetter(cresettings.DefaultGetter) + if err := loadSettings(s, SettingsPath()); err != nil { + return nil, err + } + return s, nil +} + +// newLimitsFactory builds the limits factory over settings. +// +// AtomicSettings is a settings.Getter and not a settings.Registry, so the +// factory polls it rather than subscribing - which is why a reload only has to +// swap the getter for every limit in the process to follow it. +func newLimitsFactory(lggr logger.Logger, settings *loop.AtomicSettings) limits.Factory { + return limits.Factory{ + Settings: settings, + Meter: beholder.GetMeter(), + Logger: logger.Named(lggr, "Limits"), + } +} + +// Run builds the services for a binary hosting capabilities: it serves the +// settings reload endpoint the node calls, and registers each capability once +// the process starts. +// +// The capabilities are returned alongside the registration service rather than +// wrapped by it, so the bootstrapper supervises each of them in its own right +// and their health is reported separately. +func Run(dependencies Dependencies, sc standalone.StandaloneConfig, caps ...Capability) ([]services.Service, error) { + if dependencies.settings == nil { + return nil, errors.New("dependencies were not built by this package's Dependency") + } + if sc.Mux == nil { + return nil, errors.New("standalone config has no HTTP mux to serve the reload endpoint on") + } + + // Registered during construction, which is when the bootstrapper expects + // routes: it starts serving the mux only once every service has started. + sc.Mux.HandleFunc(ReloadPath(), reloadHandler(sc.Logger, dependencies.settings, dependencies.settingsPath)) + + svcs := make([]services.Service, 0, len(caps)+1) + for _, c := range caps { + svcs = append(svcs, c) + } + svcs = append(svcs, newRegistrar(sc.Logger, dependencies, caps)) + return svcs, nil +} + +// reloadHandler re-reads the settings file and swaps it in. 200 means every limit +// in this process now resolves against the new settings; 500 means none of them +// do and the previous settings are still in force. +func reloadHandler(lggr logger.Logger, settings *loop.AtomicSettings, path string) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + if err := loadSettings(settings, path); err != nil { + lggr.Errorw("Failed to reload settings", "err", err, "path", path) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + lggr.Infow("Reloaded settings", "path", path) + fmt.Fprintln(w, "ok") + } +} + +// registrar initialises each capability and puts it in the registry, then takes +// them back out on shutdown. +// +// It is a service rather than something Run does inline because both halves need +// a context: Initialise and Add take one, and Run has none to give - the +// bootstrapper builds services first and starts them after. +type registrar struct { + services.Service + eng *services.Engine + + deps Dependencies + caps []Capability +} + +func newRegistrar(lggr logger.Logger, deps Dependencies, caps []Capability) *registrar { + r := ®istrar{deps: deps, caps: caps} + r.Service, r.eng = services.Config{ + Name: "CapabilityRegistrar", + Start: r.start, + Close: r.close, + }.NewServiceEngine(lggr) + return r +} + +func (r *registrar) start(ctx context.Context) error { + for i, c := range r.caps { + if err := c.Initialise(ctx, &r.deps); err != nil { + return fmt.Errorf("failed to initialise capability %d: %w", i, err) + } + info, err := c.Info(ctx) + if err != nil { + return fmt.Errorf("failed to read capability %d info: %w", i, err) + } + if err := r.deps.CapabilityRegistry.Add(ctx, c); err != nil { + return fmt.Errorf("failed to register capability %s: %w", info.ID, err) + } + r.eng.Infow("Registered capability", "capabilityID", info.ID, "type", info.CapabilityType) + } + return nil +} + +// close deregisters what start registered. Failures are logged rather than +// returned: the process is going away, and a capability left in a registry that +// is also going away is not worth failing shutdown over. +func (r *registrar) close() error { + ctx, cancel := r.eng.NewCtx() + defer cancel() + for _, c := range r.caps { + info, err := c.Info(ctx) + if err != nil { + r.eng.Warnw("Failed to read capability info while deregistering", "err", err) + continue + } + if err := r.deps.CapabilityRegistry.Remove(ctx, info.ID); err != nil { + r.eng.Warnw("Failed to deregister capability", "capabilityID", info.ID, "err", err) + } + } + return nil +} diff --git a/libs/standalone/capability/embed.go b/libs/standalone/capability/embed.go new file mode 100644 index 000000000..7fd4fd56e --- /dev/null +++ b/libs/standalone/capability/embed.go @@ -0,0 +1,76 @@ +package capability + +import ( + "context" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" + common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// defaultEmbeddedDonID is the DON an embedded instance belongs to. Embedding runs +// one DON in one process, so there is only ever the one, and requiring it to be +// configured would mean typing the same number into every embedded run. +const defaultEmbeddedDonID = 1 + +// embeddedConfig is the embedded form's settings. It keeps the DON ID, which an +// embedded instance can still be told, and drops the proxy port, which it cannot +// use: there is no node registry behind an embedded run to dial. +type embeddedConfig struct { + CapabilityDonID uint32 `usage:"on-chain DON ID to report for the capabilities this process hosts"` +} + +// embedded is one embedded instance's view of its host: the base registry holding +// exactly the capabilities this binary registers, and the settings and limits +// built over the same dumped file the configured form reads. +// +// The registry is the whole difference. A configured instance sits beside a node +// and asks it for anything it does not host; an embedded one has no node, so +// there is nothing behind the capabilities in this process. Resolving an ID this +// binary did not register fails, which is the honest answer - the alternative +// would be dialling a proxy that is not there. +// +// Settings are not the difference. The node still writes the file and still calls +// the reload endpoint, and an embedded run with no node simply finds no file and +// resolves every limit to its compiled-in default. +type embedded struct { + lggr logger.Logger + cfg embeddedConfig +} + +var _ common.BootstrapDependency[Dependencies] = (*embedded)(nil) + +func (d *embedded) Namespace() string { return "capabilities" } + +func (d *embedded) Config() any { return &d.cfg } + +func (d *embedded) Dependencies() []common.BootstrapCommand { return nil } + +// ForEmbedding returns instance i's dependency, so an already-embedded dependency +// embedded again is that instance's rather than a nesting of them. +// +// Every instance gets the same value here: instances share one process, and what +// this resolves - a registry of the capabilities in that process, and the +// settings file behind it - is the process's rather than any one instance's. +func (d *embedded) ForEmbedding(int) common.BootstrapDependency[Dependencies] { return d } + +func (d *embedded) Get(context.Context, common.CommonConfig) (Dependencies, error) { + settings, err := newSettings(d.lggr) + if err != nil { + return Dependencies{}, err + } + + d.lggr.Infow("Using in-process capability registry", "donID", d.cfg.CapabilityDonID) + + // nil proxy: only what this binary registers is resolvable, and the registry + // metadata calls report that rather than inventing a DON. + return Dependencies{ + LimitsFactory: newLimitsFactory(d.lggr, settings), + CRESettings: settings, + CapabilityRegistry: newOverlayRegistry(d.lggr, registry.NewBaseRegistry(d.lggr), nil), + CapabilityDonID: d.cfg.CapabilityDonID, + lggr: d.lggr, + settings: settings, + settingsPath: SettingsPath(), + }, nil +} diff --git a/libs/standalone/capability/registry.go b/libs/standalone/capability/registry.go new file mode 100644 index 000000000..0e2d6fc79 --- /dev/null +++ b/libs/standalone/capability/registry.go @@ -0,0 +1,198 @@ +package capability + +import ( + "context" + "errors" + "fmt" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// errNoProxy is what an embedded instance answers with when asked something only +// the node's registry knows. Embedding replaces the node, and there is no +// on-chain registry behind an embedded run: the DONs, nodes and capability +// configurations it would report do not exist. +var errNoProxy = errors.New("no registry proxy: an embedded instance only knows the capabilities this binary registered") + +// overlayRegistry resolves capabilities this binary registered before asking the +// node's registry proxy for anything else. +// +// Local first is not an optimisation. A capability this process hosts is a value +// it already holds, so resolving it locally hands back the implementation itself +// rather than a gRPC client looping back into this same process - and it works +// before this binary has announced anything, which is what lets one capability +// here call another during startup. +// +// proxy is nil for an embedded instance, which has no node to ask: then this is +// the base registry and nothing more, and the metadata calls fail rather than +// answering with something invented. +type overlayRegistry struct { + lggr logger.Logger + local core.CapabilitiesRegistryBase + proxy core.AddressableCapabilitiesRegistry // nil when embedded +} + +var _ core.CapabilitiesRegistry = (*overlayRegistry)(nil) + +func newOverlayRegistry(lggr logger.Logger, local core.CapabilitiesRegistryBase, proxy core.AddressableCapabilitiesRegistry) *overlayRegistry { + return &overlayRegistry{lggr: logger.Named(lggr, "OverlayRegistry"), local: local, proxy: proxy} +} + +// Add registers c in this binary's own registry, by value. +// +// It is not forwarded to the proxy: announcing a capability to the node means +// telling it an address to dial, which is a different call (AddAt) made with the +// address this process serves that capability at. Holding the value here is what +// makes it resolvable in-process; announcing it is what makes it resolvable +// elsewhere, and the two are not the same registration. +func (r *overlayRegistry) Add(ctx context.Context, c capabilities.BaseCapability) error { + return r.local.Add(ctx, c) +} + +// Remove drops a capability from the local registry, and from the node's registry +// if it was announced there. A capability absent from the proxy is not an error: +// this binary may never have announced it. +func (r *overlayRegistry) Remove(ctx context.Context, id string) error { + err := r.local.Remove(ctx, id) + if r.proxy == nil { + return err + } + if perr := r.proxy.Remove(ctx, id); perr != nil { + r.lggr.Debugw("capability not removed from the registry proxy", "capabilityID", id, "err", perr) + } + return err +} + +func (r *overlayRegistry) Get(ctx context.Context, id string) (capabilities.BaseCapability, error) { + return overlayGet(ctx, r, id, r.local.Get, proxyGet(r, func(p core.AddressableCapabilitiesRegistry) getFn[capabilities.BaseCapability] { + return p.Get + })) +} + +func (r *overlayRegistry) GetTrigger(ctx context.Context, id string) (capabilities.TriggerCapability, error) { + return overlayGet(ctx, r, id, r.local.GetTrigger, proxyGet(r, func(p core.AddressableCapabilitiesRegistry) getFn[capabilities.TriggerCapability] { + return p.GetTrigger + })) +} + +func (r *overlayRegistry) GetExecutable(ctx context.Context, id string) (capabilities.ExecutableCapability, error) { + return overlayGet(ctx, r, id, r.local.GetExecutable, proxyGet(r, func(p core.AddressableCapabilitiesRegistry) getFn[capabilities.ExecutableCapability] { + return p.GetExecutable + })) +} + +// List returns everything this binary registered plus everything the node knows +// about, with local entries winning on ID so a capability hosted here is returned +// as the value rather than as a client dialling back into this process. +func (r *overlayRegistry) List(ctx context.Context) ([]capabilities.BaseCapability, error) { + local, err := r.local.List(ctx) + if err != nil { + return nil, err + } + if r.proxy == nil { + return local, nil + } + + remote, err := r.proxy.List(ctx) + if err != nil { + // The local half is still usable and still correct; a proxy that cannot + // be reached should not blank the capabilities this process holds. + r.lggr.Warnw("failed to list capabilities from the registry proxy", "err", err) + return local, nil + } + + seen := make(map[string]bool, len(local)) + for _, c := range local { + info, ierr := c.Info(ctx) + if ierr != nil { + return nil, fmt.Errorf("failed to read local capability info: %w", ierr) + } + seen[info.ID] = true + } + for _, c := range remote { + info, ierr := c.Info(ctx) + if ierr != nil { + r.lggr.Warnw("skipping remote capability whose info could not be read", "err", ierr) + continue + } + if !seen[info.ID] { + local = append(local, c) + } + } + return local, nil +} + +// --- metadata: only the node's registry knows any of this --- + +func (r *overlayRegistry) LocalNode(ctx context.Context) (capabilities.Node, error) { + if r.proxy == nil { + return capabilities.Node{}, errNoProxy + } + return r.proxy.LocalNode(ctx) +} + +func (r *overlayRegistry) NodeByPeerID(ctx context.Context, peerID ragetypes.PeerID) (capabilities.Node, error) { + if r.proxy == nil { + return capabilities.Node{}, errNoProxy + } + return r.proxy.NodeByPeerID(ctx, peerID) +} + +func (r *overlayRegistry) ConfigForCapability(ctx context.Context, capabilityID string, donID uint32) (capabilities.CapabilityConfiguration, error) { + if r.proxy == nil { + return capabilities.CapabilityConfiguration{}, errNoProxy + } + return r.proxy.ConfigForCapability(ctx, capabilityID, donID) +} + +func (r *overlayRegistry) DONsForCapability(ctx context.Context, capabilityID string) ([]capabilities.DONWithNodes, error) { + if r.proxy == nil { + return nil, errNoProxy + } + return r.proxy.DONsForCapability(ctx, capabilityID) +} + +func (r *overlayRegistry) DONByID(ctx context.Context, donID uint32) (capabilities.DON, error) { + if r.proxy == nil { + return capabilities.DON{}, errNoProxy + } + return r.proxy.DONByID(ctx, donID) +} + +// getFn is the shape the three resolving calls share on either registry. +type getFn[T capabilities.BaseCapability] func(ctx context.Context, id string) (T, error) + +// proxyGet returns the proxy's resolving call, or nil when there is no proxy, so +// overlayGet has one thing to check rather than two. +func proxyGet[T capabilities.BaseCapability](r *overlayRegistry, pick func(core.AddressableCapabilitiesRegistry) getFn[T]) getFn[T] { + if r.proxy == nil { + return nil + } + return pick(r.proxy) +} + +// overlayGet tries local, then the proxy. A local miss is expected rather than +// exceptional - most capabilities live elsewhere - so its error is only reported +// if the proxy cannot resolve the ID either, where it is the more useful half of +// the answer: it says what this binary does host. +func overlayGet[T capabilities.BaseCapability](ctx context.Context, r *overlayRegistry, id string, local, remote getFn[T]) (T, error) { + var zero T + + got, localErr := local(ctx, id) + if localErr == nil { + return got, nil + } + if remote == nil { + return zero, localErr + } + + got, remoteErr := remote(ctx, id) + if remoteErr == nil { + return got, nil + } + return zero, fmt.Errorf("capability %s not found locally (%w) or in the registry proxy: %w", id, localErr, remoteErr) +} diff --git a/libs/standalone/capability/settings.go b/libs/standalone/capability/settings.go new file mode 100644 index 000000000..6ad6cd265 --- /dev/null +++ b/libs/standalone/capability/settings.go @@ -0,0 +1,68 @@ +// Package capability holds the conventions shared between the core node and the +// standalone capability runner binaries it launches. +// +// A runner runs under the empty LOOP: the node supervises its liveness over +// go-plugin but exposes no RPCs to it, so CRE settings reach the runner through +// the filesystem instead. The node dumps each update to a conventional path and +// then hits the runner's reload endpoint; both processes share a container, so +// os.TempDir() resolves to the same place on either side. +package capability + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +const ( + // SettingsDirName names the directory, under os.TempDir(), that CRE settings + // are dumped to. + SettingsDirName = "cre_limits" + + // SettingsFileName is the name of the file CRE settings are dumped to, and + // the path suffix of the runner's reload endpoint: /reload/. + // + // A limit's effective value is resolved out of this same settings payload by + // settings.Setting.GetOrDefault, so there is no separate limits file: + // reloading this one reloads limits too. + SettingsFileName = "settings.txt" + + // ReloadPathPrefix is the route prefix the runner serves reload requests on. + ReloadPathPrefix = "/reload/" +) + +// SettingsPath is the file CRE settings are dumped to. Both sides resolve it the +// same way, which is the whole contract: the node writes it, the runner reads it. +func SettingsPath() string { + return filepath.Join(os.TempDir(), SettingsDirName, SettingsFileName) +} + +// ReloadPath is the route the runner serves reload requests for the settings file +// on: /reload/settings.txt. +func ReloadPath() string { return ReloadPathPrefix + SettingsFileName } + +// loadSettings reads the dumped settings file into s. +// +// A missing file is not an error: nothing has been dumped yet, so s keeps the +// getter it was built with and every limit resolves to its compiled-in default. +// That is the same state a LOOP starts in before its first update arrives. +func loadSettings(s *loop.AtomicSettings, path string) error { + b, err := os.ReadFile(path) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + if err != nil { + return fmt.Errorf("failed to read settings file %s: %w", path, err) + } + // Hash is left empty: the node owns the hash of an update, and what is on + // disk is only ever a copy of one. Nothing downstream compares it. + if err := s.Store(core.SettingsUpdate{Settings: string(b)}); err != nil { + return fmt.Errorf("failed to apply settings from %s: %w", path, err) + } + return nil +} diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl index dcb743071..0e534e41b 100644 --- a/libs/standalone/gen/bootstrap.go.tmpl +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -4,7 +4,7 @@ import ( "context" "fmt" - contract "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" "github.com/smartcontractkit/chainlink-common/pkg/services" ) @@ -27,7 +27,7 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( bs *Bootstrapper, fn func(ctx context.Context, cfg *StandaloneConfig, {{range RangeNum .}}dep{{.}} T{{.}}, {{end}}) []services.Service, {{- range RangeNum .}} - bootDep{{.}} contract.BootstrapDependency[T{{.}}], + bootDep{{.}} common.BootstrapDependency[T{{.}}], {{- end}} ) error { return bs.run(func(index int, embed bool) instanceServices { @@ -44,11 +44,11 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( return fn(ctx, cfg, {{range RangeNum .}}value{{.}}, {{end}}), nil } }, - []contract.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}, {{end}} }, + []common.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}, {{end}} }, // The embedded form of each dependency, so its settings are registered on the command that // resolves it. Instance 0's, since a form is asked for its settings and not for its index; // each instance builds its own when it starts. - []contract.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}.ForEmbedding(0), {{end}} }, + []common.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}.ForEmbedding(0), {{end}} }, ) } {{- end }} diff --git a/crecore/registry/local_registry.go b/libs/x/registry/local_registry.go similarity index 100% rename from crecore/registry/local_registry.go rename to libs/x/registry/local_registry.go diff --git a/crecore/registry/registry.go b/libs/x/registry/registry.go similarity index 100% rename from crecore/registry/registry.go rename to libs/x/registry/registry.go diff --git a/libs/x/registry/registrytest/registrytest.go b/libs/x/registry/registrytest/registrytest.go new file mode 100644 index 000000000..443a7457a --- /dev/null +++ b/libs/x/registry/registrytest/registrytest.go @@ -0,0 +1,139 @@ +// Package registrytest serves a registry over an in-memory listener and hands out in-memory +// capability addresses, so tests exercise the real dial-by-address path without binding ports. +package registrytest + +import ( + "context" + "fmt" + "net" + "sync" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + + registrypb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/pb" + + "github.com/smartcontractkit/capabilities/libs/x/registry" +) + +func Serve(t testing.TB, reg *registry.Registry) *grpc.ClientConn { + t.Helper() + + s := grpc.NewServer() + registrypb.RegisterCapabilitiesRegistryServer(s, registry.NewServer(reg)) + + lis := bufconn.Listen(1 << 20) + go func() { _ = s.Serve(lis) }() + t.Cleanup(s.Stop) + + conn, err := grpc.NewClient("passthrough:///registry", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { + return lis.DialContext(ctx) + }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("failed to dial the test registry: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + return conn +} + +// AddrBook hands out in-memory capability addresses and dials them back, so a +// client's dial-by-address path runs for real without needing ports. +// +// Addresses use the passthrough scheme: gRPC would otherwise parse a bare +// "host:port" as a URI scheme and try to resolve it via DNS. +type AddrBook struct { + mu sync.Mutex + n int + listeners map[string]*bufconn.Listener + dials map[string]int +} + +func NewAddrBook() *AddrBook { + return &AddrBook{ + listeners: map[string]*bufconn.Listener{}, + dials: map[string]int{}, + } +} + +// Listen registers a new listener and returns it. Its Addr is unique per call, +// so a caller that opens one listener per capability announces distinct +// addresses, exactly as it would with port 0. +func (b *AddrBook) Listen() net.Listener { + b.mu.Lock() + defer b.mu.Unlock() + + b.n++ + name := fmt.Sprintf("passthrough:///cap-%d", b.n) + lis := bufconn.Listen(1 << 20) + b.listeners[name] = lis + return &namedListener{Listener: lis, addr: memAddr(name)} +} + +// Target returns the address for an explicitly named listener, registering it. +func (b *AddrBook) Target(name string) string { return "passthrough:///" + name } + +// Serve registers srv under name and starts serving it, returning the address a +// registry should hand out for it. +func (b *AddrBook) Serve(t testing.TB, name string, srv *grpc.Server) string { + t.Helper() + + lis := bufconn.Listen(1 << 20) + target := b.Target(name) + + b.mu.Lock() + b.listeners[target] = lis + b.mu.Unlock() + + go func() { _ = srv.Serve(lis) }() + t.Cleanup(srv.Stop) + + return target +} + +// DialOption resolves the addresses this book handed out. Unknown addresses +// fail, so "the registry returned an address nobody serves" is observable rather +// than a hang. +func (b *AddrBook) DialOption() grpc.DialOption { + return grpc.WithContextDialer(func(ctx context.Context, target string) (net.Conn, error) { + b.mu.Lock() + // The passthrough resolver strips the scheme before dialing, so accept + // either form. + lis, ok := b.listeners[target] + if !ok { + lis, ok = b.listeners["passthrough:///"+target] + } + b.dials[target]++ + b.mu.Unlock() + + if !ok { + return nil, fmt.Errorf("nothing serving %s", target) + } + return lis.DialContext(ctx) + }) +} + +// DialCount reports how many transport dials were made to name, which is how a +// test asserts that connections are reused rather than reopened per call. +func (b *AddrBook) DialCount(name string) int { + b.mu.Lock() + defer b.mu.Unlock() + return b.dials[name] + b.dials["passthrough:///"+name] +} + +type memAddr string + +func (a memAddr) Network() string { return "bufconn" } +func (a memAddr) String() string { return string(a) } + +// namedListener overrides bufconn's fixed Addr so each listener has its own. +type namedListener struct { + net.Listener + addr memAddr +} + +func (l *namedListener) Addr() net.Addr { return l.addr } diff --git a/crecore/registry/server.go b/libs/x/registry/server.go similarity index 100% rename from crecore/registry/server.go rename to libs/x/registry/server.go diff --git a/crecore/registry/server_test.go b/libs/x/registry/server_test.go similarity index 82% rename from crecore/registry/server_test.go rename to libs/x/registry/server_test.go index ee13e2a8f..0cc123dd2 100644 --- a/crecore/registry/server_test.go +++ b/libs/x/registry/server_test.go @@ -6,9 +6,6 @@ package registry_test import ( "context" "errors" - "fmt" - "net" - "sync" "testing" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" @@ -18,17 +15,16 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" - "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/proto" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" - registrypb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/pb" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-protos/cre/go/values" - "github.com/smartcontractkit/capabilities/crecore/registry" + "github.com/smartcontractkit/capabilities/libs/x/registry" + "github.com/smartcontractkit/capabilities/libs/x/registry/registrytest" "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" ) @@ -41,7 +37,7 @@ import ( // pass it - book, when the test serves fakes through one; otherwise, just credentials, since // AddHandle's dial-out is a production requirement, not conditional on what the test's own client // does. -func newRegistry(t *testing.T, book *AddrBook) *registry.Registry { +func newRegistry(t *testing.T, book *registrytest.AddrBook) *registry.Registry { t.Helper() return newRegistryWith(t, book, notSynced) } @@ -53,7 +49,7 @@ func notSynced() (*registry.LocalRegistry, error) { } // newRegistryWith is newRegistry against a given snapshot, for the metadata tests. -func newRegistryWith(t *testing.T, book *AddrBook, snapshot func() (*registry.LocalRegistry, error)) *registry.Registry { +func newRegistryWith(t *testing.T, book *registrytest.AddrBook, snapshot func() (*registry.LocalRegistry, error)) *registry.Registry { t.Helper() opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} if book != nil { @@ -81,18 +77,18 @@ func snapshot(t *testing.T, self ragetypes.PeerID, snap *registry.Snapshot) func // newClient serves reg and returns a client for it, with capability dials // resolved through book when one is given. -func newClient(t *testing.T, reg *registry.Registry, book *AddrBook) *client.Client { +func newClient(t *testing.T, reg *registry.Registry, book *registrytest.AddrBook) *client.Client { t.Helper() var opts []grpc.DialOption if book != nil { opts = append(opts, book.DialOption(), grpc.WithTransportCredentials(insecure.NewCredentials())) } - return client.New(logger.Test(t), Serve(t, reg), opts...) + return client.New(logger.Test(t), registrytest.Serve(t, reg), opts...) } // serveCapabilityAt registers impl in book under name and returns its address. -func serveCapabilityAt(t *testing.T, book *AddrBook, name string, +func serveCapabilityAt(t *testing.T, book *registrytest.AddrBook, name string, impl capabilities.BaseCapability, capType capabilities.CapabilityType) string { t.Helper() @@ -208,7 +204,7 @@ func TestAddAt_CarriesCapabilityTypeVerbatim(t *testing.T) { capabilities.CapabilityTypeCombined, } { t.Run(string(capType), func(t *testing.T) { - book := NewAddrBook() + book := registrytest.NewAddrBook() info := capabilities.MustNewCapabilityInfo("c@1.0.0", capType, "c") impl := &fakeCombined{ fakeExecutable: &fakeExecutable{info: info}, @@ -253,7 +249,7 @@ func TestAddAt_RejectsEmptyAddress(t *testing.T) { func TestServer_Remove(t *testing.T) { ctx := context.Background() - book := NewAddrBook() + book := registrytest.NewAddrBook() impl := &fakeExecutable{ info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), } @@ -280,7 +276,7 @@ func TestServer_GetExecutableDialsHandleAddress(t *testing.T) { outputs, err := values.NewMap(map[string]any{"out": int64(3)}) require.NoError(t, err) - book := NewAddrBook() + book := registrytest.NewAddrBook() impl := &fakeExecutable{ info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), response: capabilities.CapabilityResponse{Value: outputs}, @@ -302,7 +298,7 @@ func TestServer_GetExecutableDialsHandleAddress(t *testing.T) { func TestServer_ReusesConnectionPerAddress(t *testing.T) { ctx := context.Background() - book := NewAddrBook() + book := registrytest.NewAddrBook() impl := &fakeExecutable{ info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), } @@ -329,7 +325,7 @@ func TestServer_ReusesConnectionPerAddress(t *testing.T) { func TestServer_GetTriggerRejectsExecutableOnlyCapability(t *testing.T) { ctx := context.Background() - book := NewAddrBook() + book := registrytest.NewAddrBook() impl := &fakeExecutable{ info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), } @@ -348,7 +344,7 @@ func TestServer_GetTriggerRejectsExecutableOnlyCapability(t *testing.T) { func TestServer_CombinedCapabilityServesBothSurfaces(t *testing.T) { ctx := context.Background() - book := NewAddrBook() + book := registrytest.NewAddrBook() info := capabilities.MustNewCapabilityInfo("both@1.0.0", capabilities.CapabilityTypeCombined, "both") impl := &fakeCombined{ fakeExecutable: &fakeExecutable{info: info}, @@ -379,7 +375,7 @@ func TestServer_GetUnknownCapability(t *testing.T) { func TestServer_List(t *testing.T) { ctx := context.Background() - book := NewAddrBook() + book := registrytest.NewAddrBook() impl := &fakeExecutable{ info: capabilities.MustNewCapabilityInfo("good@1.0.0", capabilities.CapabilityTypeAction, "good"), } @@ -401,7 +397,7 @@ func TestServer_List(t *testing.T) { func TestServer_MissingCredentialsFailsRatherThanFallingBackToInsecure(t *testing.T) { ctx := context.Background() - book := NewAddrBook() + book := registrytest.NewAddrBook() impl := &fakeExecutable{ info: capabilities.MustNewCapabilityInfo("act@1.0.0", capabilities.CapabilityTypeAction, "act"), } @@ -585,123 +581,3 @@ type fakeCombined struct { func (f *fakeCombined) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { return f.fakeExecutable.Info(ctx) } - -func Serve(t testing.TB, reg *registry.Registry) *grpc.ClientConn { - t.Helper() - - s := grpc.NewServer() - registrypb.RegisterCapabilitiesRegistryServer(s, registry.NewServer(reg)) - - lis := bufconn.Listen(1 << 20) - go func() { _ = s.Serve(lis) }() - t.Cleanup(s.Stop) - - conn, err := grpc.NewClient("passthrough:///registry", - grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { - return lis.DialContext(ctx) - }), - grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("failed to dial the test registry: %v", err) - } - t.Cleanup(func() { _ = conn.Close() }) - - return conn -} - -// AddrBook hands out in-memory capability addresses and dials them back, so a -// client's dial-by-address path runs for real without needing ports. -// -// Addresses use the passthrough scheme: gRPC would otherwise parse a bare -// "host:port" as a URI scheme and try to resolve it via DNS. -type AddrBook struct { - mu sync.Mutex - n int - listeners map[string]*bufconn.Listener - dials map[string]int -} - -func NewAddrBook() *AddrBook { - return &AddrBook{ - listeners: map[string]*bufconn.Listener{}, - dials: map[string]int{}, - } -} - -// Listen registers a new listener and returns it. Its Addr is unique per call, -// so a caller that opens one listener per capability announces distinct -// addresses, exactly as it would with port 0. -func (b *AddrBook) Listen() net.Listener { - b.mu.Lock() - defer b.mu.Unlock() - - b.n++ - name := fmt.Sprintf("passthrough:///cap-%d", b.n) - lis := bufconn.Listen(1 << 20) - b.listeners[name] = lis - return &namedListener{Listener: lis, addr: memAddr(name)} -} - -// Target returns the address for an explicitly named listener, registering it. -func (b *AddrBook) Target(name string) string { return "passthrough:///" + name } - -// Serve registers srv under name and starts serving it, returning the address a -// registry should hand out for it. -func (b *AddrBook) Serve(t testing.TB, name string, srv *grpc.Server) string { - t.Helper() - - lis := bufconn.Listen(1 << 20) - target := b.Target(name) - - b.mu.Lock() - b.listeners[target] = lis - b.mu.Unlock() - - go func() { _ = srv.Serve(lis) }() - t.Cleanup(srv.Stop) - - return target -} - -// DialOption resolves the addresses this book handed out. Unknown addresses -// fail, so "the registry returned an address nobody serves" is observable rather -// than a hang. -func (b *AddrBook) DialOption() grpc.DialOption { - return grpc.WithContextDialer(func(ctx context.Context, target string) (net.Conn, error) { - b.mu.Lock() - // The passthrough resolver strips the scheme before dialing, so accept - // either form. - lis, ok := b.listeners[target] - if !ok { - lis, ok = b.listeners["passthrough:///"+target] - } - b.dials[target]++ - b.mu.Unlock() - - if !ok { - return nil, fmt.Errorf("nothing serving %s", target) - } - return lis.DialContext(ctx) - }) -} - -// DialCount reports how many transport dials were made to name, which is how a -// test asserts that connections are reused rather than reopened per call. -func (b *AddrBook) DialCount(name string) int { - b.mu.Lock() - defer b.mu.Unlock() - return b.dials[name] + b.dials["passthrough:///"+name] -} - -type memAddr string - -func (a memAddr) Network() string { return "bufconn" } -func (a memAddr) String() string { return string(a) } - -// namedListener overrides bufconn's fixed Addr so each listener has its own. -type namedListener struct { - net.Listener - addr memAddr -} - -func (l *namedListener) Addr() net.Addr { return l.addr } diff --git a/crecore/registry/syncer.go b/libs/x/registry/syncer.go similarity index 100% rename from crecore/registry/syncer.go rename to libs/x/registry/syncer.go diff --git a/crecore/registry/syncer_test.go b/libs/x/registry/syncer_test.go similarity index 100% rename from crecore/registry/syncer_test.go rename to libs/x/registry/syncer_test.go From b4546ffd7f82eb12057b6d42eac49480b6302ea4 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Fri, 14 Aug 2026 16:00:11 -0400 Subject: [PATCH 19/30] Some gRPC refactoring --- crecore/main.go | 28 ++-- libs/standalone/bootstrapper.go | 42 +++--- libs/standalone/capability/dependency.go | 157 ++++++++++++++++++----- libs/standalone/capability/embed.go | 50 ++++++-- libs/standalone/capability/registry.go | 45 ++++--- libs/standalone/config.go | 20 +-- libs/standalone/config_test.go | 6 - libs/standalone/grpcserver.go | 48 ------- 8 files changed, 218 insertions(+), 178 deletions(-) delete mode 100644 libs/standalone/grpcserver.go diff --git a/crecore/main.go b/crecore/main.go index aa459c6c0..393c0aeb6 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/db" + standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" "github.com/smartcontractkit/capabilities/libs/standalone/ocr" "github.com/smartcontractkit/capabilities/libs/x/registry" "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" @@ -70,23 +71,20 @@ run "docs" to write the full reference to docs/CONFIG.md.`, bootstrapper := standalone.NewBootstrapper(root, standalone.WithOtelViews(metricViews())) lggr := bootstrapper.Logger() - // This binary is the process that hosts the peer, so it takes the ocr host dependency rather - // than the proxy one: it owns the libocr networking config (the peer's own settings, and the - // keystore password that unlocks its identity) and wraps the database dependency it needs for - // that identity and the OCR discoverer table. dbDep := db.Dependency(embeddedMigrations, migrationsTable) ocrDep := ocr.Host(lggr.Named("OCR"), dbDep, ocrDiscovererTable) - // The registry always runs, so its reader is always resolved. Reading it off a chain is - // chainlink-evm's business: this names the dependency and never sees an EVM type, and the - // client it needs is a dependency of its own rather than one this binary has to hold. + readerDep := evmregistry.Dependency(lggr.Named("CapabilitiesRegistry"), evm.Dependency(lggr.Named("EVM"))) - return standalone.Run3(bootstrapper, func( + grpcDep := standalonegrpc.Dependency(lggr.Named("CoreAPI")) + + return standalone.Run4(bootstrapper, func( ctx context.Context, scfg *standalone.StandaloneConfig, factories *ocr.RageFactories, reader registry.Reader, database *sql.DB, + grpcSrv *standalonegrpc.Server, ) []services.Service { // Where the last known registry is kept. Resolving the database again costs nothing - it is // the same dependency the OCR host already took, opened once - and taking it here is what @@ -94,14 +92,16 @@ run "docs" to write the full reference to docs/CONFIG.md.`, regORM := registrysyncer.NewORM(sqlx.NewDb(database, "pgx"), scfg.Logger.Named("registry snapshots"), registrySnapshotsTable) - // Both attach to the bootstrapper's shared gRPC server (scfg.GRPCServer) rather than each - // opening a listener, so core reaches both over the single address the bootstrapper serves. + // Both attach to this process's one gRPC server rather than each opening a listener, so + // core reaches both over the single address it is configured with. regSvc := newRegistryService(cfg.CapabilitiesRegistrySyncInterval.Duration(), - scfg.Logger.Named("capabilities registry"), reader, regORM, factories.PeerID, scfg.GRPCServer) - proxySvc := newProxyService(scfg.Logger.Named("proxy service"), scfg.GRPCServer, &factories.OCRFactories) + scfg.Logger.Named("capabilities registry"), reader, regORM, factories.PeerID, grpcSrv.Registrar()) + proxySvc := newProxyService(scfg.Logger.Named("proxy service"), grpcSrv.Registrar(), &factories.OCRFactories) // The dispatcher runs the real DON-to-DON work over the same rage connection, rather than // core running it and this process merely fronting it. dispatcherSvc := newDispatcherService(cfg.Dispatcher, scfg.Logger.Named("dispatcher"), factories, regSvc.CapabilitiesRegistry()) - return []services.Service{proxySvc, regSvc, dispatcherSvc} - }, ocrDep, readerDep, dbDep) + // The server goes last: it starts serving only once the services registering on it have + // started, and services.Engine starts sub-services in the order given. + return []services.Service{proxySvc, regSvc, dispatcherSvc, grpcSrv} + }, ocrDep, readerDep, dbDep, grpcDep) } diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index bc55fc087..486a66a90 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -17,7 +17,6 @@ import ( "github.com/hashicorp/go-plugin" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" - "google.golang.org/grpc" "github.com/smartcontractkit/chainlink-common/pkg/beholder" contract "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" @@ -52,12 +51,14 @@ type StandaloneConfig struct { // be recorded. MetricsRegisterer prometheus.Registerer - // GRPCServer and Mux are this instance's shared gRPC and HTTP servers. A service registers its - // RPCs/routes on them during construction (factory, i.e. before startInstance's factory call - // returns); the bootstrapper serves both, once everything else in the instance has started - - // see startGRPCServer and startWebServer. - GRPCServer grpc.ServiceRegistrar - Mux *http.ServeMux + // Mux is this instance's shared HTTP server. A service registers its routes on it during + // construction (factory, i.e. before startInstance's factory call returns); the bootstrapper + // serves it once everything else in the instance has started - see startWebServer. + // + // There is no gRPC counterpart. The bootstrapper serves nothing of its own over gRPC, and a + // binary that needs a server takes one as a dependency (standalone/grpc) - which is also the + // only way to have more than one, as a process serving several capabilities must. + Mux *http.ServeMux } type Bootstrapper struct { @@ -283,7 +284,7 @@ func (b *Bootstrapper) runInstances(ctx context.Context, stop context.CancelFunc // for shutdown. Everything it registers is closed in reverse order by close, which puts an // instance's services ahead of the dependencies they resolved during start. func (b *Bootstrapper) startInstance(ctx context.Context, index, count int, factory instanceServices) error { - cfg, grpcServer := b.instanceConfig(index, count) + cfg := b.instanceConfig(index, count) svcs, err := factory(ctx, cfg) if err != nil { @@ -306,9 +307,9 @@ func (b *Bootstrapper) startInstance(ctx context.Context, index, count int, fact } b.registerCloser(checker) - // Everything else has started, so every service has registered what it will on cfg.Mux and - // cfg.GRPCServer by now: only now do the shared servers start actually serving. Instance i takes - // its configured port plus i, so several instances can serve their own without being configured + // Everything else has started, so every service has registered what it will on cfg.Mux by + // now: only now does the shared server start actually serving. Instance i takes its + // configured port plus i, so several instances can serve their own without being configured // a port each. web, err := startWebServer(ctx, cfg.Logger, b.observability.http.portFor(index), cfg.Mux, checker) if err != nil { @@ -316,33 +317,22 @@ func (b *Bootstrapper) startInstance(ctx context.Context, index, count int, fact } b.registerCloser(web) - grpcSrv, err := startGRPCServer(ctx, cfg.Logger, int(b.observability.grpc.portFor(index)), grpcServer) - if err != nil { - return err - } - b.registerCloser(grpcSrv) - return nil } // instanceConfig builds the StandaloneConfig handed to one instance's factory - the process-wide // values, the logger and registerer that belong to that instance alone, and a fresh shared HTTP -// mux and gRPC server for services to register on - plus the concrete gRPC server, which -// startInstance needs to serve after everything else has started but which a factory has no -// business being able to Serve or GracefulStop directly (cfg only exposes the narrower -// grpc.ServiceRegistrar). +// mux for services to register routes on. // // It deliberately does not say which instance this is. A service is written once and knows nothing // about embedding; everything that has to differ between instances is a dependency, replaced by its // embedded form before the service is handed it (see BootstrapDependency.ForEmbedding). -func (b *Bootstrapper) instanceConfig(index, count int) (*StandaloneConfig, *grpc.Server) { +func (b *Bootstrapper) instanceConfig(index, count int) *StandaloneConfig { cfg := *b.config cfg.Mux = http.NewServeMux() - grpcServer := grpc.NewServer() - cfg.GRPCServer = grpcServer if count == 1 { - return &cfg, grpcServer + return &cfg } cfg.Logger = logger.Sugared(logger.Named(b.config.Logger, "instance."+strconv.Itoa(index))) @@ -351,7 +341,7 @@ func (b *Bootstrapper) instanceConfig(index, count int) (*StandaloneConfig, *grp // not see, and registering the same collector a second time is an error. cfg.MetricsRegisterer = prometheus.WrapRegistererWith( prometheus.Labels{"instance": strconv.Itoa(index)}, prometheus.DefaultRegisterer) - return &cfg, grpcServer + return &cfg } // instanceName names an instance's aggregated service. It carries the index when there is more diff --git a/libs/standalone/capability/dependency.go b/libs/standalone/capability/dependency.go index d85812651..603a22392 100644 --- a/libs/standalone/capability/dependency.go +++ b/libs/standalone/capability/dependency.go @@ -10,8 +10,10 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/smartcontractkit/capabilities/libs/standalone" + standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" registryclient "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" @@ -49,6 +51,13 @@ type Dependencies struct { settings *loop.AtomicSettings // settingsPath is the file the reload endpoint re-reads. settingsPath string + // proxy is the node's registry, which capabilities hosted here are announced + // to by address. nil when embedded: there is no node to announce to, and the + // capabilities are resolved in-process as values. + proxy core.AddressableRegistryBase + // servers opens one gRPC server per capability. nil when embedded, which + // serves nothing. + servers *standalonegrpc.Factory // closers are the connections Get opened, torn down by Close. closers []func() error } @@ -83,14 +92,23 @@ type capabilityConfig struct { // key, and the registry is how a capability reaches the ones it does not host. // Resolving them separately would mean three dependencies agreeing on one // settings file and one proxy address. -func Dependency(lggr logger.Logger) common.BootstrapDependency[Dependencies] { +// +// servers is the gRPC factory this binary serves its capabilities with: one +// server per capability, since the registry addresses a capability by the address +// serving it. Taken as a dependency rather than built here so its settings are +// registered and documented like any other's. +func Dependency(lggr logger.Logger, servers common.BootstrapDependency[*standalonegrpc.Factory]) common.BootstrapDependency[Dependencies] { // Wrapped so the connections Get dials are made at most once however many // services resolve this. - return common.OnceBootstrapper[Dependencies](&dependency{lggr: lggr}) + return common.OnceBootstrapper[Dependencies](&dependency{ + lggr: lggr, + servers: servers, + }) } type dependency struct { - lggr logger.Logger + lggr logger.Logger + servers common.BootstrapDependency[*standalonegrpc.Factory] capabilityConfig } @@ -100,20 +118,24 @@ func (d *dependency) Namespace() string { return "capabilities" } func (d *dependency) Config() any { return &d.capabilityConfig } -func (d *dependency) Dependencies() []common.BootstrapCommand { return nil } +func (d *dependency) Dependencies() []common.BootstrapCommand { + return []common.BootstrapCommand{d.servers} +} // ForEmbedding returns the form with no proxy behind it: an embedded instance // has no node to ask, so its registry holds only what this binary registers. See // embedded. // -// The index is not used: what this dependency resolves belongs to the process -// rather than to an instance, so every instance shares the one form (and, through -// OnceBootstrapper, the one value it resolves to). -func (d *dependency) ForEmbedding(int) common.BootstrapDependency[Dependencies] { - return &embedded{lggr: d.lggr, cfg: embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}} +// The gRPC factory is embedded rather than passed through, so what an embedded +// capability serves on is whatever the factory says instance i gets. Whether that +// is one factory for the process or one each is the factory's to decide, and +// deciding it here would be this dependency asserting something about another's +// internals. +func (d *dependency) ForEmbedding(i int) common.BootstrapDependency[Dependencies] { + return &embedded{lggr: d.lggr, servers: d.servers.ForEmbedding(i), cfg: &embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}} } -func (d *dependency) Get(ctx context.Context, _ common.CommonConfig) (Dependencies, error) { +func (d *dependency) Get(ctx context.Context, cc common.CommonConfig) (Dependencies, error) { settings, err := newSettings(d.lggr) if err != nil { return Dependencies{}, err @@ -130,6 +152,11 @@ func (d *dependency) Get(ctx context.Context, _ common.CommonConfig) (Dependenci } proxy := registryclient.New(d.lggr, conn) + servers, err := d.servers.Get(ctx, cc) + if err != nil { + return Dependencies{}, fmt.Errorf("failed to get gRPC server factory: %w", err) + } + return Dependencies{ LimitsFactory: newLimitsFactory(d.lggr, settings), CRESettings: settings, @@ -138,6 +165,8 @@ func (d *dependency) Get(ctx context.Context, _ common.CommonConfig) (Dependenci lggr: d.lggr, settings: settings, settingsPath: SettingsPath(), + proxy: proxy, + servers: servers, closers: []func() error{proxy.Close, conn.Close}, }, nil } @@ -166,13 +195,20 @@ func newLimitsFactory(lggr logger.Logger, settings *loop.AtomicSettings) limits. } } -// Run builds the services for a binary hosting capabilities: it serves the -// settings reload endpoint the node calls, and registers each capability once -// the process starts. +// Run builds the services for a binary hosting caps: it serves the settings +// reload endpoint the node calls, and gives each capability a gRPC server and a +// registration once the process starts. +// +// A capability gets a server of its own rather than sharing one, because the +// registry addresses a capability by the address serving it and most of the RPCs +// reached through that address carry nothing to tell two capabilities apart. That +// is also how the LOOP transport does it - one grpc.Server per capability behind +// its own broker connection - so a binary hosting several is not a thing this +// gives up. // // The capabilities are returned alongside the registration service rather than -// wrapped by it, so the bootstrapper supervises each of them in its own right -// and their health is reported separately. +// wrapped by it, so the bootstrapper supervises each of them in its own right and +// their health is reported separately. func Run(dependencies Dependencies, sc standalone.StandaloneConfig, caps ...Capability) ([]services.Service, error) { if dependencies.settings == nil { return nil, errors.New("dependencies were not built by this package's Dependency") @@ -208,18 +244,39 @@ func reloadHandler(lggr logger.Logger, settings *loop.AtomicSettings, path strin } } -// registrar initialises each capability and puts it in the registry, then takes -// them back out on shutdown. +// registrar initialises each capability, makes it reachable, and takes it back +// out on shutdown. +// +// Reachable means three things, in order: the local registry holds it, so +// anything else in this process resolves it as a value; a gRPC server of its own +// is serving it; and the node's registry knows that server's address. Announcing +// last is deliberate - the announcement is what invites traffic, so nothing is +// announced until it can be served. // -// It is a service rather than something Run does inline because both halves need -// a context: Initialise and Add take one, and Run has none to give - the -// bootstrapper builds services first and starts them after. +// It owns the servers it opens rather than returning them as services, because +// there is one per capability and how many there are is only known here: the +// capabilities are initialised before their types can be read, and the types are +// what decide which RPCs each server carries. +// +// It is a service rather than something Run does inline because all of it needs a +// context, and Run has none to give: the bootstrapper builds services first and +// starts them after. type registrar struct { services.Service eng *services.Engine deps Dependencies caps []Capability + + // hosted is what start got as far as making reachable, so close undoes + // exactly that - a start that failed part-way leaves the rest alone. + hosted []hosted +} + +// hosted is one capability that is being served, and the server serving it. +type hosted struct { + id string + server *standalonegrpc.Server // nil when embedded, which serves nothing } func newRegistrar(lggr logger.Logger, deps Dependencies, caps []Capability) *registrar { @@ -244,25 +301,65 @@ func (r *registrar) start(ctx context.Context) error { if err := r.deps.CapabilityRegistry.Add(ctx, c); err != nil { return fmt.Errorf("failed to register capability %s: %w", info.ID, err) } + r.hosted = append(r.hosted, hosted{id: info.ID}) + + if err := r.serve(ctx, c, info); err != nil { + return err + } r.eng.Infow("Registered capability", "capabilityID", info.ID, "type", info.CapabilityType) } return nil } -// close deregisters what start registered. Failures are logged rather than -// returned: the process is going away, and a capability left in a registry that -// is also going away is not worth failing shutdown over. +// serve binds c to a gRPC server of its own and, when there is a node to tell, +// announces that server's address to its registry. +// +// The server is opened either way. Serving is what makes the capability callable +// from outside this process, and that is worth having under `embed` too - an +// embedded instance's capabilities are reachable in-process as values, but the +// address is what anything else has to go through. Only the announcement needs a +// node, so only the announcement is conditional. +func (r *registrar) serve(ctx context.Context, c Capability, info capabilities.CapabilityInfo) error { + server, err := r.deps.servers.New(ctx, logger.Named(r.eng, info.ID)) + if err != nil { + return fmt.Errorf("failed to open a server for capability %s: %w", info.ID, err) + } + r.hosted[len(r.hosted)-1].server = server + + if err := registryclient.RegisterCapability(r.eng, server.Registrar(), c, info.CapabilityType); err != nil { + return fmt.Errorf("failed to serve capability %s: %w", info.ID, err) + } + if err := server.Start(ctx); err != nil { + return fmt.Errorf("failed to start the server for capability %s: %w", info.ID, err) + } + + if r.deps.proxy == nil { + return nil + } + if err := r.deps.proxy.AddAt(ctx, info.ID, info.CapabilityType, server.Address()); err != nil { + return fmt.Errorf("failed to announce capability %s at %s: %w", info.ID, server.Address(), err) + } + return nil +} + +// close undoes what start did, in reverse: stop inviting traffic (the registry +// entry, local and announced, which overlayRegistry.Remove drops from both), then +// stop answering it (the server). +// +// Failures are logged rather than returned. The process is going away, and a +// stale entry in a registry that cannot reach it any more is not worth failing +// shutdown over - the registry fails to dial it and drops it. func (r *registrar) close() error { ctx, cancel := r.eng.NewCtx() defer cancel() - for _, c := range r.caps { - info, err := c.Info(ctx) - if err != nil { - r.eng.Warnw("Failed to read capability info while deregistering", "err", err) - continue + + for i := len(r.hosted) - 1; i >= 0; i-- { + h := r.hosted[i] + if err := r.deps.CapabilityRegistry.Remove(ctx, h.id); err != nil { + r.eng.Warnw("Failed to deregister capability", "capabilityID", h.id, "err", err) } - if err := r.deps.CapabilityRegistry.Remove(ctx, info.ID); err != nil { - r.eng.Warnw("Failed to deregister capability", "capabilityID", info.ID, "err", err) + if h.server != nil { + r.eng.ErrorIfFn(h.server.Close, "Failed to stop the server for capability "+h.id) } } return nil diff --git a/libs/standalone/capability/embed.go b/libs/standalone/capability/embed.go index 7fd4fd56e..0163ac382 100644 --- a/libs/standalone/capability/embed.go +++ b/libs/standalone/capability/embed.go @@ -2,6 +2,9 @@ package capability import ( "context" + "fmt" + + standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" @@ -34,36 +37,56 @@ type embeddedConfig struct { // the reload endpoint, and an embedded run with no node simply finds no file and // resolves every limit to its compiled-in default. type embedded struct { - lggr logger.Logger - cfg embeddedConfig + lggr logger.Logger + servers common.BootstrapDependency[*standalonegrpc.Factory] + + // cfg is shared with the configured form that produced this one, and with + // every other instance's form, so all of them read the settings the flags were + // bound to rather than a copy of the defaults. + cfg *embeddedConfig } var _ common.BootstrapDependency[Dependencies] = (*embedded)(nil) func (d *embedded) Namespace() string { return "capabilities" } -func (d *embedded) Config() any { return &d.cfg } +func (d *embedded) Config() any { return d.cfg } -func (d *embedded) Dependencies() []common.BootstrapCommand { return nil } +func (d *embedded) Dependencies() []common.BootstrapCommand { + return []common.BootstrapCommand{d.servers} +} -// ForEmbedding returns instance i's dependency, so an already-embedded dependency -// embedded again is that instance's rather than a nesting of them. +// ForEmbedding returns instance i's form, so an already-embedded dependency +// embedded again is that instance's rather than a nesting of them. The settings +// are carried over by pointer, so every instance still reads the ones the flags +// were bound to. // -// Every instance gets the same value here: instances share one process, and what -// this resolves - a registry of the capabilities in that process, and the -// settings file behind it - is the process's rather than any one instance's. -func (d *embedded) ForEmbedding(int) common.BootstrapDependency[Dependencies] { return d } +// The gRPC factory is embedded in turn rather than reused as-is: how it serves +// instance i is its own business, and this only has to ask it the same question +// the configured form did. +func (d *embedded) ForEmbedding(i int) common.BootstrapDependency[Dependencies] { + return &embedded{lggr: d.lggr, servers: d.servers.ForEmbedding(i), cfg: d.cfg} +} -func (d *embedded) Get(context.Context, common.CommonConfig) (Dependencies, error) { +func (d *embedded) Get(ctx context.Context, cc common.CommonConfig) (Dependencies, error) { settings, err := newSettings(d.lggr) if err != nil { return Dependencies{}, err } + // The one factory every instance resolves, so the capabilities of a whole + // embed run take consecutive ports rather than every instance starting over + // at the same one. + servers, err := d.servers.Get(ctx, cc) + if err != nil { + return Dependencies{}, fmt.Errorf("failed to get gRPC server factory: %w", err) + } + d.lggr.Infow("Using in-process capability registry", "donID", d.cfg.CapabilityDonID) - // nil proxy: only what this binary registers is resolvable, and the registry - // metadata calls report that rather than inventing a DON. + // No proxy: only what this binary registers is resolvable, and the registry + // metadata calls say so rather than inventing a DON. The capabilities are + // still served - see registrar.serve. return Dependencies{ LimitsFactory: newLimitsFactory(d.lggr, settings), CRESettings: settings, @@ -72,5 +95,6 @@ func (d *embedded) Get(context.Context, common.CommonConfig) (Dependencies, erro lggr: d.lggr, settings: settings, settingsPath: SettingsPath(), + servers: servers, }, nil } diff --git a/libs/standalone/capability/registry.go b/libs/standalone/capability/registry.go index 0e2d6fc79..df6692d74 100644 --- a/libs/standalone/capability/registry.go +++ b/libs/standalone/capability/registry.go @@ -18,14 +18,14 @@ import ( // configurations it would report do not exist. var errNoProxy = errors.New("no registry proxy: an embedded instance only knows the capabilities this binary registered") -// overlayRegistry resolves capabilities this binary registered before asking the -// node's registry proxy for anything else. +// overlayRegistry resolves capabilities this binary hosts before asking the +// node's registry for anything else. // // Local first is not an optimisation. A capability this process hosts is a value -// it already holds, so resolving it locally hands back the implementation itself -// rather than a gRPC client looping back into this same process - and it works -// before this binary has announced anything, which is what lets one capability -// here call another during startup. +// it already holds, so resolving it locally hands back the implementation rather +// than a gRPC client looping back into this same process - and it works before +// this binary has announced anything, which is what lets one capability here call +// another during startup. // // proxy is nil for an embedded instance, which has no node to ask: then this is // the base registry and nothing more, and the metadata calls fail rather than @@ -44,18 +44,17 @@ func newOverlayRegistry(lggr logger.Logger, local core.CapabilitiesRegistryBase, // Add registers c in this binary's own registry, by value. // -// It is not forwarded to the proxy: announcing a capability to the node means -// telling it an address to dial, which is a different call (AddAt) made with the -// address this process serves that capability at. Holding the value here is what -// makes it resolvable in-process; announcing it is what makes it resolvable -// elsewhere, and the two are not the same registration. +// It is not forwarded to the node. Announcing a capability there means naming the +// address serving it, which the registrar does with AddAt once it has opened that +// capability's server - holding the value here is what makes it resolvable +// in-process, and the two are not the same registration. func (r *overlayRegistry) Add(ctx context.Context, c capabilities.BaseCapability) error { return r.local.Add(ctx, c) } -// Remove drops a capability from the local registry, and from the node's registry -// if it was announced there. A capability absent from the proxy is not an error: -// this binary may never have announced it. +// Remove drops a capability from the local registry, and from the node's if it +// was announced there. A capability absent from the node's is not an error: this +// binary may never have announced it. func (r *overlayRegistry) Remove(ctx context.Context, id string) error { err := r.local.Remove(ctx, id) if r.proxy == nil { @@ -85,9 +84,9 @@ func (r *overlayRegistry) GetExecutable(ctx context.Context, id string) (capabil })) } -// List returns everything this binary registered plus everything the node knows -// about, with local entries winning on ID so a capability hosted here is returned -// as the value rather than as a client dialling back into this process. +// List returns everything this binary hosts plus everything the node knows about, +// with local entries winning on ID so a capability hosted here comes back as the +// value rather than as a client dialling back into this process. func (r *overlayRegistry) List(ctx context.Context) ([]capabilities.BaseCapability, error) { local, err := r.local.List(ctx) if err != nil { @@ -99,8 +98,8 @@ func (r *overlayRegistry) List(ctx context.Context) ([]capabilities.BaseCapabili remote, err := r.proxy.List(ctx) if err != nil { - // The local half is still usable and still correct; a proxy that cannot - // be reached should not blank the capabilities this process holds. + // The local half is still usable and still correct; a node that cannot be + // reached should not blank the capabilities this process holds. r.lggr.Warnw("failed to list capabilities from the registry proxy", "err", err) return local, nil } @@ -166,8 +165,8 @@ func (r *overlayRegistry) DONByID(ctx context.Context, donID uint32) (capabiliti // getFn is the shape the three resolving calls share on either registry. type getFn[T capabilities.BaseCapability] func(ctx context.Context, id string) (T, error) -// proxyGet returns the proxy's resolving call, or nil when there is no proxy, so -// overlayGet has one thing to check rather than two. +// proxyGet returns the node registry's resolving call, or nil when there is none, +// so overlayGet has one thing to check rather than two. func proxyGet[T capabilities.BaseCapability](r *overlayRegistry, pick func(core.AddressableCapabilitiesRegistry) getFn[T]) getFn[T] { if r.proxy == nil { return nil @@ -175,9 +174,9 @@ func proxyGet[T capabilities.BaseCapability](r *overlayRegistry, pick func(core. return pick(r.proxy) } -// overlayGet tries local, then the proxy. A local miss is expected rather than +// overlayGet tries local, then the node. A local miss is expected rather than // exceptional - most capabilities live elsewhere - so its error is only reported -// if the proxy cannot resolve the ID either, where it is the more useful half of +// if the node cannot resolve the ID either, where it is the more useful half of // the answer: it says what this binary does host. func overlayGet[T capabilities.BaseCapability](ctx context.Context, r *overlayRegistry, id string, local, remote getFn[T]) (T, error) { var zero T diff --git a/libs/standalone/config.go b/libs/standalone/config.go index db0457beb..b96087063 100644 --- a/libs/standalone/config.go +++ b/libs/standalone/config.go @@ -28,7 +28,6 @@ const ( chipIngressNamespace = "chip-ingress" pyroscopeNamespace = "pyroscope" httpNamespace = "http" - grpcNamespace = "grpc" // Legacy env prefixes for the two map-valued telemetry settings. pkg/config/flags has no // map support, so those are []string of key=value pairs here; a host sets one env var per @@ -94,19 +93,6 @@ func (c HTTPConfig) portFor(index int) uint16 { return c.Port + uint16(index) } -// GRPCConfig is the shared gRPC server: whatever services register on StandaloneConfig.GRPCServer -// during construction, served on one address per instance instead of each opening its own. -// -// Every instance serves its own, so under `embed` instance i listens on Port+i. -type GRPCConfig struct { - Port uint16 `usage:"port serving the shared gRPC server. Instance i of an embed run listens on this port plus i" validate:"required"` -} - -// portFor is the port instance i serves on; see HTTPConfig.portFor. -func (c GRPCConfig) portFor(index int) uint16 { - return c.Port + uint16(index) -} - // observability is every process-wide observability config, registered together by // NewBootstrapper and consumed once the command runs and they have been decoded. type observability struct { @@ -115,12 +101,11 @@ type observability struct { chipIngress ChipIngressConfig pyroscope PyroscopeConfig http HTTPConfig - grpc GRPCConfig } // defaultObservability is what the flags are bound to and decoded into, so an unset setting keeps -// the value it is given here. HTTP and gRPC have no default: both are `validate:"required"`, -// so leaving either unconfigured fails at startup rather than silently picking a port. +// the value it is given here. The HTTP port has no default: it is `validate:"required"`, so +// leaving it unconfigured fails at startup rather than silently picking one. func defaultObservability() observability { return observability{ tracing: TracingConfig{SamplingRatio: 1}, @@ -142,7 +127,6 @@ func (o *observability) namespaced() []struct { {chipIngressNamespace, &o.chipIngress}, {pyroscopeNamespace, &o.pyroscope}, {httpNamespace, &o.http}, - {grpcNamespace, &o.grpc}, } } diff --git a/libs/standalone/config_test.go b/libs/standalone/config_test.go index e3589533b..d97f79112 100644 --- a/libs/standalone/config_test.go +++ b/libs/standalone/config_test.go @@ -13,12 +13,6 @@ func TestHTTPConfigPortFor(t *testing.T) { assert.Equal(t, uint16(9092), cfg.portFor(2)) } -func TestGRPCConfigPortFor(t *testing.T) { - cfg := GRPCConfig{Port: 50051} - assert.Equal(t, uint16(50051), cfg.portFor(0)) - assert.Equal(t, uint16(50053), cfg.portFor(2)) -} - func TestParsePairs(t *testing.T) { pairs, err := parsePairs("telemetry.attributes", []string{"env=staging", "region=eu-west-1"}) require.NoError(t, err) diff --git a/libs/standalone/grpcserver.go b/libs/standalone/grpcserver.go deleted file mode 100644 index 96fba29b1..000000000 --- a/libs/standalone/grpcserver.go +++ /dev/null @@ -1,48 +0,0 @@ -package standalone - -import ( - "context" - "errors" - "fmt" - "net" - - "google.golang.org/grpc" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" -) - -// grpcServer serves one instance's shared gRPC server: whatever services registered on -// StandaloneConfig.GRPCServer during construction. -type grpcServer struct { - lggr logger.Logger - server *grpc.Server - listener net.Listener -} - -// startGRPCServer listens on port and serves server, which already has every service's RPCs -// registered on it from earlier in that instance's construction. Port 0 asks the OS for an -// ephemeral port, which is logged once bound. -func startGRPCServer(ctx context.Context, lggr logger.Logger, port int, server *grpc.Server) (*grpcServer, error) { - // An explicit listener resolves port 0 before Serve, so the chosen port can be logged. - var lc net.ListenConfig - listener, err := lc.Listen(ctx, "tcp", fmt.Sprintf(":%d", port)) - if err != nil { - return nil, fmt.Errorf("failed to listen on port %d: %w", port, err) - } - - g := &grpcServer{lggr: lggr, server: server, listener: listener} - - go func() { - if err := server.Serve(listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) { - g.lggr.Errorw("gRPC server stopped", "err", err) - } - }() - - lggr.Infow("Serving gRPC", "address", listener.Addr().String()) - return g, nil -} - -func (g *grpcServer) Close() error { - g.server.GracefulStop() - return nil -} From c6700eb47d2e854e44aff816ae9590b67ccfe7c4 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Tue, 18 Aug 2026 10:39:03 -0400 Subject: [PATCH 20/30] WIP (untested and I haven't read the code enough) consensus standalone --- .gitignore | 1 + consensus/action/capability.go | 192 +++-- consensus/action/capability_test.go | 154 ++-- consensus/go.mod | 227 ++++- consensus/go.sum | 794 +++++++++++++++--- consensus/main.go | 110 ++- .../oracle/types/value_consensus_types.pb.go | 2 +- consensus/protos/consensus.pb.go | 74 ++ consensus/protos/consensus.proto | 16 + consensus/protos/consensus_server_gen.go | 136 +++ consensus/protos/gen/main.go | 25 + crecore/go.mod | 4 +- crecore/go.sum | 4 +- crecore/main.go | 2 +- crecore/pingpong/main.go | 103 +++ crecore/pingpong/run_4.sh | 126 +++ crecore/pingpong/service.go | 225 +++++ crecore/pingpong/setup/main.go | 96 +++ crecore/proxy_service.go | 14 +- crecore/signer_server.go | 95 +++ libs/go.mod | 2 +- libs/go.sum | 2 + libs/ocr/database.go | 182 ++++ libs/ocr/digest.go | 117 +++ libs/ocr/oracle.go | 159 ++++ libs/standalone/capability/capability.go | 14 +- libs/standalone/capability/dependency.go | 21 +- libs/standalone/grpc/dependency.go | 159 ++++ libs/standalone/grpc/dependency_test.go | 117 +++ libs/standalone/grpc/server.go | 125 +++ libs/standalone/ocr/dependency.go | 10 + libs/standalone/ocr/host.go | 23 +- libs/standalone/ocr/keyring.go | 47 ++ libs/standalone/ocr/proxy.go | 26 +- libs/standalone/ocr/signer.go | 207 +++++ .../protohelpers/generator/downloader.go | 489 +++++++++++ .../protohelpers/generator/generator.go | 238 ++++++ .../protohelpers/generator/plugin.go | 80 ++ .../protohelpers/generator/protoc.go | 69 ++ .../protohelpers/generator/stage.go | 54 ++ libs/standalone/protohelpers/protoc/main.go | 63 ++ .../protohelpers/protoc/server.go.tmpl | 232 +++++ libs/x/registry/ocr.go | 82 ++ libs/x/registry/server.go | 12 + libs/x/registrysyncer/local_registry.go | 12 +- 45 files changed, 4589 insertions(+), 353 deletions(-) create mode 100644 consensus/protos/consensus.pb.go create mode 100644 consensus/protos/consensus.proto create mode 100644 consensus/protos/consensus_server_gen.go create mode 100644 consensus/protos/gen/main.go create mode 100644 crecore/pingpong/main.go create mode 100755 crecore/pingpong/run_4.sh create mode 100644 crecore/pingpong/service.go create mode 100644 crecore/pingpong/setup/main.go create mode 100644 crecore/signer_server.go create mode 100644 libs/ocr/database.go create mode 100644 libs/ocr/digest.go create mode 100644 libs/ocr/oracle.go create mode 100644 libs/standalone/grpc/dependency.go create mode 100644 libs/standalone/grpc/dependency_test.go create mode 100644 libs/standalone/grpc/server.go create mode 100644 libs/standalone/ocr/signer.go create mode 100644 libs/standalone/protohelpers/generator/downloader.go create mode 100644 libs/standalone/protohelpers/generator/generator.go create mode 100644 libs/standalone/protohelpers/generator/plugin.go create mode 100644 libs/standalone/protohelpers/generator/protoc.go create mode 100644 libs/standalone/protohelpers/generator/stage.go create mode 100644 libs/standalone/protohelpers/protoc/main.go create mode 100644 libs/standalone/protohelpers/protoc/server.go.tmpl create mode 100644 libs/x/registry/ocr.go diff --git a/.gitignore b/.gitignore index 9536aafa4..0ad3bbae9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ coverage.txt # Dependency directories (remove the comment below to include it) # vendor/ proto_vendor +.proto_vendor # START - Chainlink Local (CLL) artifacts .local diff --git a/consensus/action/capability.go b/consensus/action/capability.go index 55d75eb16..a1191c08c 100644 --- a/consensus/action/capability.go +++ b/consensus/action/capability.go @@ -3,7 +3,6 @@ package action import ( "context" "encoding/hex" - "encoding/json" "errors" "fmt" "strconv" @@ -24,11 +23,12 @@ import ( "github.com/smartcontractkit/capabilities/consensus/oracle/plugin" "github.com/smartcontractkit/capabilities/consensus/oracle/transmitter" "github.com/smartcontractkit/capabilities/consensus/oracle/types" + "github.com/smartcontractkit/capabilities/consensus/protos" + libsocr "github.com/smartcontractkit/capabilities/libs/ocr" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/requests" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/consensus/server" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" @@ -39,8 +39,11 @@ import ( "github.com/smartcontractkit/chainlink-protos/cre/go/values" valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2/types" + "github.com/prometheus/client_golang/prometheus" + + "github.com/smartcontractkit/libocr/commontypes" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ) const ( @@ -63,11 +66,11 @@ type ConsensusCapabilityConfig struct { MaxRequestOutcomeSize int } -var _ server.ConsensusCapability = &consensusCapability{} +var _ protos.ConsensusCapability = &consensusCapability{} type consensusCapability struct { lggr logger.Logger - oracle core.Oracle + oracle Oracle reqStore *requests.Store[*oracle.ConsensusRequest] reqHandler *requests.Handler[*oracle.ConsensusRequest, oracle.ConsensusResponse] limitsFactory limits.Factory @@ -92,12 +95,62 @@ func (s *storeStatsCollector) SetRequestCount(requestCount int) { s.requestStoreRequests.Record(context.Background(), int64(requestCount)) } +// Dependencies are what a consensus capability needs from wherever it is hosted: +// where to read its OCR configuration, the networking and identity to run that +// configuration under, and who else is in the DON. +// +// They are taken here rather than through an Initialise the host calls, because +// a capability that is not yet usable is only a way to be used too early: with +// these, what New returns is ready, and Start runs it. +type Dependencies struct { + // DonID is the capability DON this process was spawned for, which together + // with the capability ID selects this oracle's configuration. + DonID uint32 + + // Registry supplies that configuration, digest included. + Registry core.OCRConfigRegistry + + // Endpoints, Offchain and Onchain come from whoever holds the node's peer: + // the transport, and the keys this oracle signs with. + Endpoints ocrtypes.BinaryNetworkEndpointFactory + Offchain ocrtypes.OffchainKeyring + Onchain ocr3types.OnchainKeyring[[]byte] + + // Bootstrappers are the peers to dial before this oracle has heard of + // anyone; the registry says who the oracle set is, not where it is. + Bootstrappers []commontypes.BootstrapperLocator + + LimitsFactory limits.Factory + Metrics prometheus.Registerer + + // NewOracle builds the oracle this capability runs, defaulting to a real one + // over the configuration and networking above. A test replaces it to drive + // the reporting plugin directly, which is the part of an oracle a test of + // this capability is about. + NewOracle func(libsocr.OracleArgs) (Oracle, error) +} + +// Oracle is what this capability does with a libocr oracle: run it, and stop it. +type Oracle interface { + Start() error + Close() error +} + // NewConsensusCapability creates a new ConsensusCapability with the given logger, clock, and response cache expiry time. The // response cache expiry controls how long a response for a given request is cached before it is considered expired and evicted. This allows // the capability to respond to slow requests sent after consensus has been reached. -func NewConsensusCapability(lggr logger.Logger, clock clockwork.Clock, responseCacheExpiry time.Duration, - limitsFactory limits.Factory, -) (*consensusCapability, error) { +// +// The oracle is built here and started by Start: building it needs the +// configuration, which is a question for the registry, and starting it means +// joining a protocol, which is a thing to do when the process is ready to serve +// rather than while it is still being assembled. +func NewConsensusCapability( + lggr logger.Logger, + clock clockwork.Clock, + responseCacheExpiry time.Duration, + cfg ConsensusCapabilityConfig, + deps Dependencies, +) (protos.ConsensusCapability, error) { metrics, err := metrics.NewMetrics() if err != nil { return nil, fmt.Errorf("error creating metrics: %w", err) @@ -108,88 +161,77 @@ func NewConsensusCapability(lggr logger.Logger, clock clockwork.Clock, responseC requestStoreRequests: metrics.PendingConsensusRequests, }) - return &consensusCapability{ + c := &consensusCapability{ lggr: lggr, reqStore: reqStore, reqHandler: requests.NewHandler(lggr, reqStore, clock, responseCacheExpiry), metrics: metrics, - limitsFactory: limitsFactory, + limitsFactory: deps.LimitsFactory, observationQuorumTracker: oracle.NewObservationQuorumTracker(), - }, nil -} - -// SetRequestTimeout is used by the reporting plugin to set the request timeout for consensus requests. The plugin -// receives the timeout after the Initialise method on the capability is called, thus it uses this method to set it on the capability. -func (c *consensusCapability) SetRequestTimeout(timeout time.Duration) { - c.requestTimeoutLock.Lock() - defer c.requestTimeoutLock.Unlock() - c.requestTimeout = timeout -} - -func (c *consensusCapability) Initialise(ctx context.Context, dependencies core.StandardCapabilitiesDependencies) error { - c.lggr.Debug("Initialising Consensus Capability") + } - if err := c.setConfiguration(dependencies.Config); err != nil { - return fmt.Errorf("error setting consensus capability configuration: %w", err) + if err = c.setConfiguration(cfg); err != nil { + return nil, fmt.Errorf("error setting consensus capability configuration: %w", err) } - reportingPlugin, err := plugin.NewReportingPluginFactory(c.lggr, c.metrics, c.reqStore, + reportingPlugin, err := plugin.NewReportingPluginFactory(lggr, c.metrics, c.reqStore, c.observationQuorumTracker, c.SetRequestTimeout, defaultKeyBundleIDForValueConsensus, c.maxRequestOutcomeSize) if err != nil { - return fmt.Errorf("error when creating reporting plugin factory: %w", err) - } - - contractTransmitter := transmitter.NewContractTransmitter(c.lggr, c.SendResponse) - - // These values set to the maximum permitted, response time for config update is not critical - localOcrConfig := ocrtypes.LocalConfig{ - BlockchainTimeout: time.Second * 20, - ContractConfigTrackerPollInterval: time.Second * 60, - ContractConfigConfirmations: 1, - ContractTransmitterTransmitTimeout: time.Second * 60, - DatabaseTimeout: time.Second * 10, - ContractConfigLoadTimeout: time.Second * 60, - DefaultMaxDurationInitialization: time.Second * 60, - } - - oracle, err := dependencies.OracleFactory.NewOracle(ctx, core.OracleArgs{ - LocalConfig: localOcrConfig, - ReportingPluginFactoryService: reportingPlugin, - ContractTransmitter: contractTransmitter, + return nil, fmt.Errorf("error when creating reporting plugin factory: %w", err) + } + + newOracle := deps.NewOracle + if newOracle == nil { + newOracle = func(args libsocr.OracleArgs) (Oracle, error) { return libsocr.NewOracle(args) } + } + + c.oracle, err = newOracle(libsocr.OracleArgs{ + CapabilityID: protos.ConsensusID, + DonID: deps.DonID, + Registry: deps.Registry, + Endpoints: deps.Endpoints, + Offchain: deps.Offchain, + Onchain: deps.Onchain, + Bootstrappers: deps.Bootstrappers, + Plugin: reportingPlugin, + Transmitter: transmitter.NewContractTransmitter(lggr, c.SendResponse), + LocalConfig: localOCRConfig, + Logger: lggr, + Metrics: deps.Metrics, }) if err != nil { - return fmt.Errorf("error when creating oracle: %w", err) + return nil, fmt.Errorf("error when creating oracle: %w", err) } - c.oracle = oracle - err = c.reqHandler.Start(context.Background()) - if err != nil { - return fmt.Errorf("error when starting request handler: %w", err) - } - - err = c.oracle.Start(context.Background()) - if err != nil { - return fmt.Errorf("error when starting oracle: %w", err) - } + return c, nil +} - c.lggr.Debug("Initialised Consensus Capability") +// localOCRConfig is set to the maximum permitted throughout: response time for a +// config update is not critical for a capability, whose configuration comes from +// a registry snapshot rather than from a chain read that has to be waited on. +var localOCRConfig = ocrtypes.LocalConfig{ + BlockchainTimeout: time.Second * 20, + ContractConfigTrackerPollInterval: time.Second * 60, + ContractConfigConfirmations: 1, + ContractTransmitterTransmitTimeout: time.Second * 60, + DatabaseTimeout: time.Second * 10, + ContractConfigLoadTimeout: time.Second * 60, + DefaultMaxDurationInitialization: time.Second * 60, +} - return nil +// SetRequestTimeout is used by the reporting plugin to set the request timeout for consensus requests. The plugin +// receives the timeout after the capability is built, thus it uses this method to set it on the capability. +func (c *consensusCapability) SetRequestTimeout(timeout time.Duration) { + c.requestTimeoutLock.Lock() + defer c.requestTimeoutLock.Unlock() + c.requestTimeout = timeout } -func (c *consensusCapability) setConfiguration(cfg string) error { +func (c *consensusCapability) setConfiguration(capabilityConfig ConsensusCapabilityConfig) error { c.valueConsensusKeyBundleID = defaultKeyBundleIDForValueConsensus - var capabilityConfig ConsensusCapabilityConfig - if len(cfg) > 0 { - err := json.Unmarshal([]byte(cfg), &capabilityConfig) - if err != nil { - return fmt.Errorf("failed to deserialize config into ConsensusCapabilityConfig: %w", err) - } - } - if capabilityConfig.MaxRequestOutcomeSize > 0 { c.maxRequestOutcomeSize = capabilityConfig.MaxRequestOutcomeSize } else { @@ -497,8 +539,18 @@ func (c *consensusCapability) SendResponse(ctx context.Context, response oracle. c.reqHandler.SendResponse(ctx, response) } -// Start is not called when running as remote standard capability, instead Initialise is called (note Close is called) +// Start joins the protocol: the request handler first, so a response has +// somewhere to go before the oracle can produce one. func (c *consensusCapability) Start(ctx context.Context) error { + if err := c.reqHandler.Start(ctx); err != nil { + return fmt.Errorf("error when starting request handler: %w", err) + } + + if err := c.oracle.Start(); err != nil { + return fmt.Errorf("error when starting oracle: %w", err) + } + + c.lggr.Debug("Started Consensus Capability") return nil } @@ -509,7 +561,7 @@ func (c *consensusCapability) Close() error { } if c.oracle != nil { - if err := c.oracle.Close(context.Background()); err != nil { + if err := c.oracle.Close(); err != nil { return fmt.Errorf("error when closing oracle: %w", err) } c.oracle = nil diff --git a/consensus/action/capability_test.go b/consensus/action/capability_test.go index aea60d7b6..ead9d592d 100644 --- a/consensus/action/capability_test.go +++ b/consensus/action/capability_test.go @@ -1,9 +1,9 @@ package action import ( + "context" "crypto/rand" "encoding/hex" - "encoding/json" "errors" "fmt" "strings" @@ -24,20 +24,16 @@ import ( "github.com/smartcontractkit/chainlink-protos/cre/go/values" "github.com/smartcontractkit/capabilities/libs/testutils" + + libsocr "github.com/smartcontractkit/capabilities/libs/ocr" ) func Test_SimpleConsensus(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -70,14 +66,8 @@ func Test_SimpleConsensus_Error(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -110,14 +100,8 @@ func Test_Report(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -141,14 +125,8 @@ func Test_ReportSupportsAptosSigningAndHashing(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -171,14 +149,8 @@ func Test_ReportSupportsStellarSigningAndHashing(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -202,14 +174,8 @@ func Test_ReportRequiresValidSigningAlgo(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -232,14 +198,8 @@ func Test_ReportRequiresValidHashingAlgo(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -262,14 +222,8 @@ func Test_ReportRequiresValidEncoderName(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{}, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -292,22 +246,10 @@ func Test_SimpleInputsSizeValidation_UserErrorSentToConsensus(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - capConfig := &ConsensusCapabilityConfig{ - MaxRequestSizeBytes: 2, - } - - capConfigJSON, err := json.Marshal(capConfig) - require.NoError(t, err) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - Config: string(capConfigJSON), - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{ + MaxRequestSizeBytes: 2, + }, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -342,23 +284,10 @@ func Test_SimpleRequestSizeValidation(t *testing.T) { lggr := logger.Test(t) ctx := t.Context() - capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, limits.Factory{Logger: lggr}) - require.NoError(t, err) - - oracleFactory := testutils.NewOracleFactory(t, lggr) - - // Set MaxRequestSizeBytes to 2MB - capConfig := &ConsensusCapabilityConfig{ - MaxRequestSizeBytes: 2000000, // 2MB - } - - capConfigJSON, err := json.Marshal(capConfig) - require.NoError(t, err) - - err = capability.Initialise(ctx, core.StandardCapabilitiesDependencies{ - Config: string(capConfigJSON), - OracleFactory: oracleFactory, - }) + capability, err := NewConsensusCapability(lggr, clockwork.NewRealClock(), time.Minute, + ConsensusCapabilityConfig{ + MaxRequestSizeBytes: 2000000, // 2MB + }, testDependencies(t, lggr)) require.NoError(t, err) servicetest.Run(t, capability) @@ -490,3 +419,32 @@ func generateRandomHexString(byteLength int) string { } return hex.EncodeToString(randomBytes) } + +// testDependencies drive the reporting plugin directly instead of joining a DON: +// these tests are about what the capability computes, and an oracle that really +// ran the protocol would need a DON to run it with. +func testDependencies(t *testing.T, lggr logger.Logger) Dependencies { + return Dependencies{ + LimitsFactory: limits.Factory{Logger: lggr}, + NewOracle: func(args libsocr.OracleArgs) (Oracle, error) { + oracle, err := testutils.NewOracleFactory(t, lggr).NewOracle(t.Context(), core.OracleArgs{ + ReportingPluginFactoryService: args.Plugin, + ContractTransmitter: args.Transmitter, + }) + if err != nil { + return nil, err + } + return contextlessOracle{t: t, oracle: oracle}, nil + }, + } +} + +// contextlessOracle adapts the test oracle, which takes a context as the node's +// oracle factory does, to the libocr shape this capability drives. +type contextlessOracle struct { + t *testing.T + oracle core.Oracle +} + +func (o contextlessOracle) Start() error { return o.oracle.Start(o.t.Context()) } +func (o contextlessOracle) Close() error { return o.oracle.Close(context.Background()) } diff --git a/consensus/go.mod b/consensus/go.mod index f9d76c19f..7b0d42994 100644 --- a/consensus/go.mod +++ b/consensus/go.mod @@ -3,15 +3,17 @@ module github.com/smartcontractkit/capabilities/consensus go 1.26.2 require ( - github.com/cloudevents/sdk-go/v2 v2.16.1 + github.com/cloudevents/sdk-go/v2 v2.16.2 github.com/google/uuid v1.6.0 github.com/jonboulle/clockwork v0.5.0 + github.com/prometheus/client_golang v1.23.2 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/cre-sdk-go v0.9.0 - github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d + github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 @@ -21,87 +23,205 @@ require ( ) require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/XSAM/otelsql v0.37.0 // indirect + cosmossdk.io/api v0.7.6 // indirect + cosmossdk.io/collections v0.4.0 // indirect + cosmossdk.io/core v0.11.0 // indirect + cosmossdk.io/depinject v1.1.0 // indirect + cosmossdk.io/errors v1.0.1 // indirect + cosmossdk.io/log v1.4.1 // indirect + cosmossdk.io/math v1.4.0 // indirect + cosmossdk.io/store v1.1.1 // indirect + cosmossdk.io/x/tx v0.13.7 // indirect + filippo.io/bigmod v0.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + filippo.io/nistec v0.0.4 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.1 // indirect + github.com/DataDog/zstd v1.5.7 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/NethermindEth/juno v0.15.11 // indirect + github.com/NethermindEth/starknet.go v0.17.1 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 // indirect + github.com/XSAM/otelsql v0.42.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/buger/jsonparser v1.1.2 // indirect + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/buger/jsonparser v1.2.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect + github.com/cockroachdb/errors v1.12.0 // indirect + github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect + github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect + github.com/cockroachdb/pebble v1.1.5 // indirect + github.com/cockroachdb/redact v1.1.6 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect + github.com/cometbft/cometbft v0.38.21 // indirect + github.com/cometbft/cometbft-db v0.14.1 // indirect + github.com/consensys/gnark-crypto v0.20.1 // indirect + github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-db v1.1.1 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect + github.com/cosmos/cosmos-sdk v0.50.14 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/gogoproto v1.7.0 // indirect + github.com/cosmos/ics23/go v0.11.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/deckarep/golang-set/v2 v2.9.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/dgraph-io/badger/v4 v4.2.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.7.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect + github.com/ethereum/go-ethereum v1.17.3 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.13.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // 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.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect - github.com/grafana/pyroscope-go v1.2.8 // indirect - github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grafana/pyroscope-go v1.3.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.3 // indirect github.com/hashicorp/go-plugin v1.8.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.14.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.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/lib/pq v1.12.3 // indirect + github.com/linxGnu/grocksdb v1.8.14 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.2.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/zerolog v1.33.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect - github.com/smartcontractkit/chain-selectors v1.0.100 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6 // indirect - github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b // indirect - github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect + github.com/smartcontractkit/chain-selectors v1.0.104 // indirect + github.com/smartcontractkit/chainlink-common/keystore v1.3.0 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect + github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d // indirect + github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect + github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect + github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect + github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/stellar/go-stellar-sdk v0.4.0 // indirect + github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xssnick/tonutils-go v1.14.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect + go.dedis.ch/fixbuf v1.0.3 // indirect + go.dedis.ch/kyber/v3 v3.1.0 // indirect + go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect @@ -109,16 +229,31 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.82.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect + pgregory.net/rapid v1.1.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common + +replace github.com/smartcontractkit/capabilities/libs => ../libs + +// Local override: cre/impl/proxy dropped peer-group proxying, only used for the capabilities +// registry move that already has its own proto. +replace github.com/smartcontractkit/chainlink-protos/cre/impl => ../../chainlink-protos/cre/impl diff --git a/consensus/go.sum b/consensus/go.sum index 38e46af17..1c8dc5c09 100644 --- a/consensus/go.sum +++ b/consensus/go.sum @@ -1,75 +1,293 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= +cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= +cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= +cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= +cosmossdk.io/core v0.11.0 h1:vtIafqUi+1ZNAE/oxLOQQ7Oek2n4S48SWLG8h/+wdbo= +cosmossdk.io/core v0.11.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= +cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E= +cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI= +cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= +cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= +cosmossdk.io/log v1.4.1 h1:wKdjfDRbDyZRuWa8M+9nuvpVYxrEOwbD/CA8hvhU8QM= +cosmossdk.io/log v1.4.1/go.mod h1:k08v0Pyq+gCP6phvdI6RCGhLf/r425UT6Rk/m+o74rU= +cosmossdk.io/math v1.4.0 h1:XbgExXFnXmF/CccPPEto40gOO7FpWu9yWNAZPN3nkNQ= +cosmossdk.io/math v1.4.0/go.mod h1:O5PkD4apz2jZs4zqFdTr16e1dcaQCc5z6lkEnrrppuk= +cosmossdk.io/store v1.1.1 h1:NA3PioJtWDVU7cHHeyvdva5J/ggyLDkyH0hGHl2804Y= +cosmossdk.io/store v1.1.1/go.mod h1:8DwVTz83/2PSI366FERGbWSH7hL6sB7HbYp8bqksNwM= +cosmossdk.io/x/tx v0.13.7 h1:8WSk6B/OHJLYjiZeMKhq7DK7lHDMyK0UfDbBMxVmeOI= +cosmossdk.io/x/tx v0.13.7/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= +filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= +filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s= +filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= -github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= -github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= -github.com/apache/arrow-go/v18 v18.3.1/go.mod h1:12QBya5JZT6PnBihi5NJTzbACrDGXYkrgjujz3MRQXU= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NethermindEth/juno v0.15.11 h1:v8nVO6ccvNx4eNmI6b6cKfGmRiucx0Y7QpgYJks6gz0= +github.com/NethermindEth/juno v0.15.11/go.mod h1:DyfDC1vz8OpoAOWdGJif97Kueo4J7yhZUtYkkFUYg20= +github.com/NethermindEth/starknet.go v0.17.1 h1:VmB81n2GX8m+bFisXVCF5Z6k+uHpDglyNkUCqTVqAJo= +github.com/NethermindEth/starknet.go v0.17.1/go.mod h1:72WzcIncBwvAUANawfRtKRR+6nUrc9eYMYs6QEbbh1Y= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 h1:/97whAzwYxMNHXeTfhAtCRzNCpyblmxCtSYpsfzCszM= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/XSAM/otelsql v0.42.0 h1:Li0xF4eJUxG2e0x3D4rvRlys1f27yJKvjTh7ljkUP5o= +github.com/XSAM/otelsql v0.42.0/go.mod h1:4mOrEv+cS1KmKzrvTktvJnstr5GtKSAK+QHvFR9OcpI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow-go/v18 v18.6.0 h1:GX/Jyd3R7mCLiECAwY9FWbbaYblie2WXBSz4Sw8fNpM= +github.com/apache/arrow-go/v18 v18.6.0/go.mod h1:gm3MiPpY82fLYK5VKPB3WoJbsiLVDfT7flD5/vHReKw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +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/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= -github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= +github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= +github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 h1:nLaJZcVAnaqch3K83AyzHfY2DmQM18/L7jvkmKSfkpI= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1/go.mod h1:6Q+F2puKpJ6zWv+R02BVnizJICf7++oRT5zwpZQAsbk= -github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= -github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCeN8rge3F/deT52w2WJMmIC5mHNUq+Ut8= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= +github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= +github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= +github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +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/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= +github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= +github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= +github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= +github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= +github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= +github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= +github.com/cosmos/cosmos-sdk v0.50.14 h1:G8CtGHFWbExa+ZpVOVAb4kFmko/R30igsYOwyzRMtgY= +github.com/cosmos/cosmos-sdk v0.50.14/go.mod h1:hrWEFMU1eoXqLJeE6VVESpJDQH67FS1nnMrQIjO2daw= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= +github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= +github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= +github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= +github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= +github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= +github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= +github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= +github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= +github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI= +github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= +github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= +github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= +github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= +github.com/gagliardetto/solana-go v1.13.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= +github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -77,46 +295,117 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= -github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= -github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8= +github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g= +github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= +github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= +github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= +github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= 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= @@ -125,163 +414,360 @@ github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/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/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= +github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +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= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +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= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924 h1:6wqsOpDXA0ZMEswN7f8hX04Y3+gXva7p5emXThtJVlI= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924/go.mod h1:v0O0Au8RE00Z89QxBE6I2q9bR9r3+RO1gLD3oaO2WB0= -github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= -github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6 h1:hms02zQQ0BPcp9CBwh/xda5KwJWdU0IIA/yjtwyRoA4= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6/go.mod h1:jueIfDkkRexwGgLbVB7vGCZlNtd383zuwi4uHHwcbqc= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6 h1:ucHu2bPDT/58AzSgnPDyp4IjnjVbrVWYD3bG5jCbXMY= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= +github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= +github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 h1:iljEJss3WOwcsMkWy72Yn2zvjw7Gyxc+RXL7r8YKM6g= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b h1:36knUpKHHAZ86K4FGWXtx8i/EQftGdk2bqCoEu/Cha8= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 h1:W0HKHO8eE8BckTRnhSdqjHKbJcnk068nEWYnWRu6tJY= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 h1:NJdGFhzT6zMaTod4QkBqVD2sg0I25iw1boOYtTpEwRo= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe/go.mod h1:z7lx7wI3XZ4u9kmUtAVdwn1BCC9T8aieWSDcuDgPTdQ= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 h1:/vkKPJoweLkRd56V4YHGRAtTG4+/JAlgklGEfvH6l4c= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 h1:LQy2j2+TdKLSWsUTUYuqmQPn8kjqCLjGI3ZJYGtDc08= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= github.com/smartcontractkit/cre-sdk-go v0.9.0 h1:MDO9HFb4tjvu4mI4gKvdO+qXP1irULxhFwlTPVBytaM= github.com/smartcontractkit/cre-sdk-go v0.9.0/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= -github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74= -github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd h1:ksFjz3ytjK4kH5HFHpLKzDS0/9gmeSuvii1rs8FlxrI= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= +github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb h1:kLHdQQkijaPGsBbtV2rJgpzVpQ96e7T10pzjNlWfK8U= +github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb/go.mod h1:4s5hj/nlMF9WV+T5Uhy4n9IYpRpzfJzT+vTKkNT7T+Y= +github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= +github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945/go.mod h1:m3pdp17i4bD50XgktkzWetcV5yaLsi7Gunbv4ZgN6qg= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stellar/go-stellar-sdk v0.4.0 h1:WBLeJk7EllU7WhwrTH1L5Mu2EhZor/xy5sVi/82JzNQ= +github.com/stellar/go-stellar-sdk v0.4.0/go.mod h1:tLKAQPxa2I5UvGMabBbUXcY3fmgYnfDudrMeK7CDX4w= +github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf h1:GY1RVbX3Hg7poPXEf6yojjP0hyypvgUgZmCqQU9D0xg= +github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf/go.mod h1:If+U9Z1W5xU97VrOgJandQT+2dN7/iOpkCrxBJEyF80= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg= +github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= +go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= +go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= +go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= +go.dedis.ch/kyber/v3 v3.1.0 h1:ghu+kiRgM5JyD9TJ0hTIxTLQlJBR/ehjWvWwYW3XsC0= +go.dedis.ch/kyber/v3 v3.1.0/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U= +go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= +go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= +go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= +go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= @@ -301,17 +787,32 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -320,56 +821,100 @@ golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5l golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -381,34 +926,41 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= +google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -422,15 +974,39 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/consensus/main.go b/consensus/main.go index 92c68f15d..158fdcd5e 100644 --- a/consensus/main.go +++ b/consensus/main.go @@ -1,24 +1,118 @@ +// Command consensus runs the consensus capability as its own binary. +// +// It hosts no node of its own: rage networking, the keys it signs with and the +// capabilities registry all come from the crecore process it is pointed at, and +// what is left here is the capability - the OCR plugin that reaches consensus +// over a workflow's observations, and the server that makes it callable. package main import ( + "context" + "log" "time" "github.com/jonboulle/clockwork" + "github.com/spf13/cobra" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/consensus/server" - "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" + "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/capabilities/consensus/action" "github.com/smartcontractkit/capabilities/consensus/metrics" - "github.com/smartcontractkit/capabilities/libs/loopserver" + "github.com/smartcontractkit/capabilities/consensus/protos" + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/capabilities/libs/standalone/capability" + standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" ) +// responseCacheExpiry is how long a response is kept after consensus is reached, +// so that a request arriving late still gets the answer rather than starting the +// round again. +const responseCacheExpiry = time.Minute + func main() { - loopserver.ServeNew("ConsensusCapability", func(s *loop.Server) loop.StandardCapabilities { - capability, err := action.NewConsensusCapability(s.Logger, clockwork.NewRealClock(), 1*time.Minute, s.LimitsFactory) + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + cfg := defaultConfig + + root := &cobra.Command{ + Use: "consensus", + Short: "The CRE consensus capability", + Long: `Runs the consensus capability, which reaches OCR consensus over the +observations a workflow's nodes make and returns the agreed result. + +It holds no keys and no peer: --ocr.proxy-address is the crecore process hosting +this node's rage identity, which also signs on this capability's behalf, and +--capabilities.proxy-url is the registry that says which DON this is and what +OCR configuration it runs under. + +Settings can come from flags, from CRE_/CL_ env vars, or from a --config file; +run "docs" to write the full reference to docs/CONFIG.md.`, + } + root.PersistentFlags().String("config", "", "Path to config file") + + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = "consensus" + if err := flags.RegisterCommandFlags(root, &cfg, opts); err != nil { + return err + } + + bootstrapper := standalone.NewBootstrapper(root, standalone.WithOtelViews(metrics.MetricViews())) + lggr := bootstrapper.Logger() + + // The proxy form, not the host one: this binary drives an oracle, it does not run a peer. + ocrDep := ocr.Proxy(lggr.Named("OCR")) + capDep := capability.Dependency(lggr.Named("Capabilities"), standalonegrpc.FactoryDependency(lggr.Named("CapabilityAPI"))) + + return standalone.Run2(bootstrapper, func( + ctx context.Context, + scfg *standalone.StandaloneConfig, + factories *ocr.OCRFactories, + deps capability.Dependencies, + ) []services.Service { + lggr := scfg.Logger.Named("consensus") + + capabilityImpl, err := action.NewConsensusCapability(lggr, clockwork.NewRealClock(), responseCacheExpiry, + cfg.ConsensusCapabilityConfig, + action.Dependencies{ + DonID: deps.CapabilityDonID, + Registry: deps.OCRConfigRegistry(), + Endpoints: factories.OCR2Endpoint, + Offchain: factories.Offchain, + Onchain: factories.Onchain, + Bootstrappers: cfg.Bootstrappers.ToBootstrapperLocators(), + LimitsFactory: deps.LimitsFactory, + Metrics: scfg.MetricsRegisterer, + }) + if err != nil { + lggr.Fatalw("Failed to create ConsensusCapability", "error", err) + } + + // Run supervises the capability and makes it reachable: registered, + // served, and announced to the node's registry. + svcs, err := capability.Run(deps, *scfg, protos.NewConsensusServer(capabilityImpl)) if err != nil { - s.Logger.Fatalw("Failed to create ConsensusCapability", "error", err) + lggr.Fatalw("Failed to host ConsensusCapability", "error", err) } - return server.NewConsensusServer(capability) - }, loop.WithOtelViews(metrics.MetricViews())) + return svcs + }, ocrDep, capDep) } + +// Config is what this binary needs that its host cannot tell it. +type Config struct { + action.ConsensusCapabilityConfig `toml:",inline"` + + // Bootstrappers are peerID@host:port entries to dial before this oracle has + // heard of anyone. The registry says who the DON's members are; it does not + // say where to find them, and an oracle that has met nobody cannot be told + // by the DON where the DON is. + Bootstrappers config.BootstrapperLocators `usage:"peerID@host:port of the DON's bootstrap peers" example:"['12D3KooWFirst@127.0.0.1:6690']"` +} + +var defaultConfig = Config{} diff --git a/consensus/oracle/types/value_consensus_types.pb.go b/consensus/oracle/types/value_consensus_types.pb.go index 5a9192abd..ee752d2a4 100644 --- a/consensus/oracle/types/value_consensus_types.pb.go +++ b/consensus/oracle/types/value_consensus_types.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: value_consensus_types.proto diff --git a/consensus/protos/consensus.pb.go b/consensus/protos/consensus.pb.go new file mode 100644 index 000000000..092c269b1 --- /dev/null +++ b/consensus/protos/consensus.pb.go @@ -0,0 +1,74 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: capabilities/internal/consensus/v1alpha/consensus.proto + +package protos + +import ( + sdk "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + _ "github.com/smartcontractkit/chainlink-protos/cre/go/tools/generator" + pb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +var File_capabilities_internal_consensus_v1alpha_consensus_proto protoreflect.FileDescriptor + +const file_capabilities_internal_consensus_v1alpha_consensus_proto_rawDesc = "" + + "\n" + + "7capabilities/internal/consensus/v1alpha/consensus.proto\x12'capabilities.internal.consensus.v1alpha\x1a\x15sdk/v1alpha/sdk.proto\x1a*tools/generator/v1alpha/cre_metadata.proto\x1a\x16values/v1/values.proto2\xad\x01\n" + + "\tConsensus\x12>\n" + + "\x06Simple\x12\".sdk.v1alpha.SimpleConsensusInputs\x1a\x10.values.v1.Value\x12A\n" + + "\x06Report\x12\x1a.sdk.v1alpha.ReportRequest\x1a\x1b.sdk.v1alpha.ReportResponse\x1a\x1d\x82\xb5\x18\x19\b\x01\x12\x15consensus@1.0.0-alphab\x06proto3" + +var file_capabilities_internal_consensus_v1alpha_consensus_proto_goTypes = []any{ + (*sdk.SimpleConsensusInputs)(nil), // 0: sdk.v1alpha.SimpleConsensusInputs + (*sdk.ReportRequest)(nil), // 1: sdk.v1alpha.ReportRequest + (*pb.Value)(nil), // 2: values.v1.Value + (*sdk.ReportResponse)(nil), // 3: sdk.v1alpha.ReportResponse +} +var file_capabilities_internal_consensus_v1alpha_consensus_proto_depIdxs = []int32{ + 0, // 0: capabilities.internal.consensus.v1alpha.Consensus.Simple:input_type -> sdk.v1alpha.SimpleConsensusInputs + 1, // 1: capabilities.internal.consensus.v1alpha.Consensus.Report:input_type -> sdk.v1alpha.ReportRequest + 2, // 2: capabilities.internal.consensus.v1alpha.Consensus.Simple:output_type -> values.v1.Value + 3, // 3: capabilities.internal.consensus.v1alpha.Consensus.Report:output_type -> sdk.v1alpha.ReportResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_capabilities_internal_consensus_v1alpha_consensus_proto_init() } +func file_capabilities_internal_consensus_v1alpha_consensus_proto_init() { + if File_capabilities_internal_consensus_v1alpha_consensus_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_capabilities_internal_consensus_v1alpha_consensus_proto_rawDesc), len(file_capabilities_internal_consensus_v1alpha_consensus_proto_rawDesc)), + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_capabilities_internal_consensus_v1alpha_consensus_proto_goTypes, + DependencyIndexes: file_capabilities_internal_consensus_v1alpha_consensus_proto_depIdxs, + }.Build() + File_capabilities_internal_consensus_v1alpha_consensus_proto = out.File + file_capabilities_internal_consensus_v1alpha_consensus_proto_goTypes = nil + file_capabilities_internal_consensus_v1alpha_consensus_proto_depIdxs = nil +} diff --git a/consensus/protos/consensus.proto b/consensus/protos/consensus.proto new file mode 100644 index 000000000..e1abb013f --- /dev/null +++ b/consensus/protos/consensus.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package capabilities.internal.consensus.v1alpha; + +import "sdk/v1alpha/sdk.proto"; +import "tools/generator/v1alpha/cre_metadata.proto"; +import "values/v1/values.proto"; + +service Consensus { + option (tools.generator.v1alpha.capability) = { + mode: MODE_DON + capability_id: "consensus@1.0.0-alpha" + }; + rpc Simple(sdk.v1alpha.SimpleConsensusInputs) returns (values.v1.Value); + rpc Report(sdk.v1alpha.ReportRequest) returns (sdk.v1alpha.ReportResponse); +} diff --git a/consensus/protos/consensus_server_gen.go b/consensus/protos/consensus_server_gen.go new file mode 100644 index 000000000..5b26e5df2 --- /dev/null +++ b/consensus/protos/consensus_server_gen.go @@ -0,0 +1,136 @@ +// Code generated by github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/protoc, DO NOT EDIT. + +package protos + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" +) + +// Avoid unused imports if there is configuration type +var _ = emptypb.Empty{} + +// ConsensusCapability is what a capability implements to be served as consensus@1.0.0-alpha. +// +// It carries no Initialise: a capability is given what it needs when it is +// built, and everything a host does to it - registering it, serving it, +// announcing it, taking it back out - belongs to the bootstrapper that hosts it +// rather than to the capability or to this server. +type ConsensusCapability interface { + Simple(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.SimpleConsensusInputs) (*capabilities.ResponseAndMetadata[*pb.Value], caperrors.Error) + + Report(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.ReportRequest) (*capabilities.ResponseAndMetadata[*sdk.ReportResponse], caperrors.Error) + + Start(ctx context.Context) error + Close() error + HealthReport() map[string]error + Name() string + Description() string + Ready() error +} + +func NewConsensusServer(capability ConsensusCapability) *ConsensusServer { + stopCh := make(chan struct{}) + return &ConsensusServer{ + consensusCapability: consensusCapability{ConsensusCapability: capability, stopCh: stopCh}, + stopCh: stopCh, + } +} + +// ConsensusServer serves the capability: it turns the untyped requests a host +// delivers into calls on the typed methods above, and is itself the +// capabilities.ExecutableAndTriggerCapability a host registers and serves. +type ConsensusServer struct { + consensusCapability + stopCh chan struct{} +} + +// Close stops answering registered triggers, then closes the capability. +// +// Nothing is deregistered here. What put this capability in a registry, and +// announced it to a node, is the host - so taking it back out is the host's too, +// and doing it from both would race a shutdown against itself. +func (c *ConsensusServer) Close() error { + if c.stopCh != nil { + close(c.stopCh) + } + + return c.consensusCapability.Close() +} + +type consensusCapability struct { + ConsensusCapability + stopCh chan struct{} +} + +func (c *consensusCapability) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { + return capabilities.NewCapabilityInfo("consensus@1.0.0-alpha", capabilities.CapabilityTypeCombined, c.ConsensusCapability.Description()) +} + +var _ capabilities.ExecutableAndTriggerCapability = (*consensusCapability)(nil) + +const ConsensusID = "consensus@1.0.0-alpha" + +func (c *consensusCapability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + return nil, fmt.Errorf("trigger %s not found", request.Method) +} + +func (c *consensusCapability) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error { + return fmt.Errorf("trigger %s not found", request.Method) +} + +func (c *consensusCapability) AckEvent(ctx context.Context, triggerId string, eventId string, method string) error { + return fmt.Errorf("trigger %s not found", method) +} + +func (c *consensusCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (c *consensusCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} + +func (c *consensusCapability) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + response := capabilities.CapabilityResponse{} + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + case "Simple": + input := &sdk.SimpleConsensusInputs{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.SimpleConsensusInputs, _ *emptypb.Empty) (*pb.Value, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ConsensusCapability.Simple(ctx, metadata, input) + if err != nil { + return nil, capabilities.ResponseMetadata{}, nil, err + } + if output == nil { + return nil, capabilities.ResponseMetadata{}, nil, fmt.Errorf("output and error is nil for method Simple(..) (if output is nil error must be present)") + } + return output.Response, output.ResponseMetadata, output.OCRAttestation, err + } + return capabilities.Execute(ctx, request, input, config, wrapped) + case "Report": + input := &sdk.ReportRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.ReportRequest, _ *emptypb.Empty) (*sdk.ReportResponse, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ConsensusCapability.Report(ctx, metadata, input) + if err != nil { + return nil, capabilities.ResponseMetadata{}, nil, err + } + if output == nil { + return nil, capabilities.ResponseMetadata{}, nil, fmt.Errorf("output and error is nil for method Report(..) (if output is nil error must be present)") + } + return output.Response, output.ResponseMetadata, output.OCRAttestation, err + } + return capabilities.Execute(ctx, request, input, config, wrapped) + default: + return response, fmt.Errorf("method %s not found", request.Method) + } +} diff --git a/consensus/protos/gen/main.go b/consensus/protos/gen/main.go new file mode 100644 index 000000000..8d8422830 --- /dev/null +++ b/consensus/protos/gen/main.go @@ -0,0 +1,25 @@ +// Command gen generates the consensus capability's protos. +// +// It lives here, rather than in the module holding the generator, so that it is +// built from the generator - and the protoc plugins - that this capability's +// go.mod pins. Updating those is then this capability's own change, and a +// capability that has not made it keeps generating exactly what it did before. +package main + +import ( + "fmt" + "os" + + "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/generator" +) + +//go:generate go run . + +func main() { + // Any capability whose protos these import is named here, so that its + // protos are compiled alongside and linked to the Go code it generated. + if err := generator.Generate(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/crecore/go.mod b/crecore/go.mod index 3b46172f2..2cc49f7ad 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -6,6 +6,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/smartcontractkit/capabilities/libs v0.0.0-20260807195051-f3d5f6d13400 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 + github.com/smartcontractkit/chainlink-common/keystore v1.3.0 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260728111445-96c471be2872 github.com/smartcontractkit/chainlink-protos/cre/impl v0.0.0-20260724132051-f39bd9ab890d github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd @@ -200,8 +201,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect - github.com/smartcontractkit/chain-selectors v1.0.100 // indirect - github.com/smartcontractkit/chainlink-common/keystore v1.3.0 // indirect + github.com/smartcontractkit/chain-selectors v1.0.104 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect diff --git a/crecore/go.sum b/crecore/go.sum index 7e0e734e2..535b02118 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -710,8 +710,8 @@ github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= -github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= +github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= diff --git a/crecore/main.go b/crecore/main.go index 393c0aeb6..6d8d8f3d5 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -96,7 +96,7 @@ run "docs" to write the full reference to docs/CONFIG.md.`, // core reaches both over the single address it is configured with. regSvc := newRegistryService(cfg.CapabilitiesRegistrySyncInterval.Duration(), scfg.Logger.Named("capabilities registry"), reader, regORM, factories.PeerID, grpcSrv.Registrar()) - proxySvc := newProxyService(scfg.Logger.Named("proxy service"), grpcSrv.Registrar(), &factories.OCRFactories) + proxySvc := newProxyService(scfg.Logger.Named("proxy service"), grpcSrv.Registrar(), factories) // The dispatcher runs the real DON-to-DON work over the same rage connection, rather than // core running it and this process merely fronting it. dispatcherSvc := newDispatcherService(cfg.Dispatcher, scfg.Logger.Named("dispatcher"), factories, regSvc.CapabilitiesRegistry()) diff --git a/crecore/pingpong/main.go b/crecore/pingpong/main.go new file mode 100644 index 000000000..2fd194a95 --- /dev/null +++ b/crecore/pingpong/main.go @@ -0,0 +1,103 @@ +// Command pingpong is a sample CRE binary: it passes messages between its instances over a libocr +// binary network endpoint, without hosting a rage peer itself. +// +// Each instance delegates its rage networking to a crecore p2p proxy (see the ocr.Proxy bootstrap +// dependency), which hosts the peer whose identity --ocr.peer-id names. So this binary needs no +// database, no keystore and no listen address of its own: it is told which peer it is and where to +// reach the process holding that peer's key, and everything else - dialling, discovery, encryption - +// happens over there. +// +// What it does with the endpoint is deliberately trivial, so that seeing the messages arrive is the +// whole point: oracle 0 says hi to 1, 1 says hi to 2, and so on round to the last, which says hi +// back to 0. Once a lap is complete 0 broadcasts that it is starting again, and the lap repeats. +// Every instance prints each message it receives. +// +// run_4.sh runs four of these against four crecore proxies on one machine. +package main + +import ( + "context" + "log" + "time" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + cfg := defaultConfig + + root := &cobra.Command{ + Use: "pingpong", + Short: "Sample app passing messages over a proxied libocr binary network endpoint", + Long: `Passes messages between its instances over a libocr binary network endpoint. + +The endpoint is not this process's own: rage networking is delegated to the crecore p2p proxy at +--ocr.proxy-address, which hosts the peer --ocr.peer-id names. --pingpong.peers lists every +participant as peerID@host:port, in the order that decides who says hi to whom; the addresses are +where those peers listen, which is how they find each other to begin with. + +Settings can come from flags, from CRE_/CL_ env vars, or from a --config file; +run "docs" to write the full reference to docs/CONFIG.md.`, + } + root.PersistentFlags().String("config", "", "Path to config file") + + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = "pingpong" + if err := flags.RegisterCommandFlags(root, &cfg, opts); err != nil { + return err + } + + bootstrapper := standalone.NewBootstrapper(root) + lggr := bootstrapper.Logger() + + // The proxy dependency, not the host one: this binary drives an endpoint, it does not run a + // peer. Which is also why it needs no database - it is told its peer ID rather than unlocking a + // keystore to find it. + ocrDep := ocr.Proxy(lggr.Named("OCR")) + + return standalone.Run1(bootstrapper, func( + ctx context.Context, + scfg *standalone.StandaloneConfig, + factories *ocr.OCRFactories, + ) []services.Service { + return []services.Service{newPingPong(scfg.Logger.Named("pingpong"), &cfg, factories)} + }, ocrDep) +} + +// Config is what one instance needs to know about the others. Its own place in the ring is not +// configured: it is wherever its peer ID appears in Peers. +type Config struct { + // Each entry is peerID@host:port, because both halves are needed and there is no sense in + // configuring them apart. The peer ID says who an oracle is - its position here is the oracle ID + // the others address it by - and the address is where its peer listens, which libocr needs to + // dial before any of them have heard of each other: a proxy knows only the announcements it has + // seen, and a fresh one has seen none. The address is the crecore --ocr.listen-addresses of the + // process hosting that peer, not its --proxy.listen-address, which is a private arrangement + // between an app and its own proxy. + Peers []string `usage:"peerID@host:port of every participant, in the order that decides who says hi to whom; this instance's own --ocr.peer-id must be one of them" validate:"required" example:"['12D3KooWFirst@127.0.0.1:6690','12D3KooWSecond@127.0.0.1:6691']"` + + StartDelay config.Duration `usage:"how long to wait before the first lap, giving the peers time to connect"` + RoundDelay config.Duration `usage:"how long to wait after a lap completes before starting the next one"` + RetryDelay config.Duration `usage:"how long oracle 0 waits for a lap to come back round before starting it again"` +} + +var defaultConfig = Config{ + StartDelay: *config.MustNewDuration(5 * time.Second), + RoundDelay: *config.MustNewDuration(2 * time.Second), + // Long enough that a lap has really stalled rather than merely being slow, short enough that a + // message dropped before the peers had connected does not end the demo. + RetryDelay: *config.MustNewDuration(5 * time.Second), +} diff --git a/crecore/pingpong/run_4.sh b/crecore/pingpong/run_4.sh new file mode 100755 index 000000000..52b083043 --- /dev/null +++ b/crecore/pingpong/run_4.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# +# Runs four crecore p2p proxies and four pingpong instances against them, on this machine. +# +# Each proxy hosts one peer, so each needs a keystore of its own to take that peer's identity from - +# crecore reads the first key ring in the database it is pointed at. That means four databases: one +# shared one would hand every proxy the same key, and four proxies claiming one peer ID is not a +# network. They are created here, seeded by ./setup, and dropped again on the next run. +# +# Because each proxy then keeps its own record of the announcements it has seen, a fresh one knows +# of nobody. The apps are configured with peerID@address for every peer, which is how each proxy is +# told where the others listen. +# +# Needs a postgres to create databases in. Point DATABASE_URL at one; the default is a local server +# with the usual postgres/postgres credentials, e.g. +# +# docker run --rm -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres --name cre-pingpong-db postgres:16 +# +# Ctrl-C stops everything. +set -euo pipefail + +cd "$(dirname "$0")" + +INSTANCES=${INSTANCES:-4} +DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable} +# One password for every shard: they are four halves of one demo, not four operators. +KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD:-pingpong-keystore-password} + +DB_PREFIX=${DB_PREFIX:-cre_pingpong} +P2P_PORT=${P2P_PORT:-6690} # what each proxy's rage peer listens on +PROXY_PORT=${PROXY_PORT:-50051} # what each proxy serves its gRPC on, and what its app dials +HEALTH_PORT=${HEALTH_PORT:-9100} # /healthz and /metrics, one port per proxy + +# The registry crecore serves alongside the proxy always runs and wants an address and an RPC. There +# is no chain here, so it is pointed at a dead one: it logs that it cannot sync, and the proxy - the +# part this demo uses - is unaffected. +REGISTRY_ADDRESS=${REGISTRY_ADDRESS:-0x0000000000000000000000000000000000000001} +EVM_HTTP_URL=${EVM_HTTP_URL:-http://127.0.0.1:1} +EVM_CHAIN_ID=${EVM_CHAIN_ID:-1} + +BIN=$(mktemp -d) +LOGS=${LOGS:-$(mktemp -d)} +pids=() + +cleanup() { + trap - INT TERM EXIT + echo + echo "stopping..." + for pid in "${pids[@]:-}"; do + [[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true + done + wait 2>/dev/null || true + rm -rf "$BIN" +} +trap cleanup INT TERM EXIT + +# psql_admin runs SQL against the server itself rather than one of the demo databases, for creating +# and dropping them. +psql_admin() { psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -c "$1"; } + +# database_url returns the url of instance $1's database, keeping the credentials and options of +# DATABASE_URL and replacing only the database name. +database_url() { + python3 - "$DATABASE_URL" "${DB_PREFIX}_$1" <<-'EOF' + import sys + from urllib.parse import urlsplit, urlunsplit + url, name = sys.argv[1], sys.argv[2] + parts = urlsplit(url) + print(urlunsplit(parts._replace(path="/" + name))) + EOF +} + +echo "building..." +go build -o "$BIN/crecore" .. +go build -o "$BIN/pingpong" . +go build -o "$BIN/setup" ./setup + +echo "preparing $INSTANCES databases and keystores..." +peers=() +for i in $(seq 0 $((INSTANCES - 1))); do + psql_admin "DROP DATABASE IF EXISTS ${DB_PREFIX}_$i" + psql_admin "CREATE DATABASE ${DB_PREFIX}_$i" + peer_id=$("$BIN/setup" -url "$(database_url "$i")" -password "$KEYSTORE_PASSWORD") + # Where instance i's peer lives: its ID, and the address the proxy hosting it listens on. + peers+=("$peer_id@127.0.0.1:$((P2P_PORT + i))") +done + +# Every peer in one comma-separated list: a peer's position here is the oracle ID the others address +# it by, so instance i is oracle i. +peer_list=$(IFS=,; echo "${peers[*]}") + +echo "starting $INSTANCES proxies..." +for i in $(seq 0 $((INSTANCES - 1))); do + "$BIN/crecore" run \ + --ocr.listen-addresses="127.0.0.1:$((P2P_PORT + i))" \ + --ocr.keystore-password="$KEYSTORE_PASSWORD" \ + --database.url="$(database_url "$i")" \ + --grpc.host="127.0.0.1" \ + --grpc.port="$((PROXY_PORT + i))" \ + --http.port="$((HEALTH_PORT + i))" \ + --capabilities-registry.address="$REGISTRY_ADDRESS" \ + --evm.chain-id="$EVM_CHAIN_ID" \ + --evm.http-url="$EVM_HTTP_URL" \ + >"$LOGS/crecore_$i.log" 2>&1 & + pids+=($!) +done + +# Long enough for each proxy to have opened its database, unlocked its key and bound its port; the +# apps retry anyway, so this only keeps the first few seconds of output tidy. +sleep 5 + +echo "starting $INSTANCES apps..." +for i in $(seq 0 $((INSTANCES - 1))); do + "$BIN/pingpong" run \ + --ocr.proxy-address="127.0.0.1:$((PROXY_PORT + i))" \ + --ocr.peer-id="${peers[$i]%@*}" \ + --http.port=""$((HEALTH_PORT + i + INSTANCES))"" \ + --pingpong.peers="$peer_list" \ + 2>"$LOGS/pingpong_$i.log" & + pids+=($!) +done + +echo +echo "logs are in $LOGS; the messages below are the apps' stdout" +echo +wait diff --git a/crecore/pingpong/service.go b/crecore/pingpong/service.go new file mode 100644 index 000000000..7e2d6fae7 --- /dev/null +++ b/crecore/pingpong/service.go @@ -0,0 +1,225 @@ +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "math" + "slices" + "strings" + "time" + + "github.com/smartcontractkit/libocr/commontypes" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +// The two messages this app sends. A lap is a chain of hi, and the broadcast marks where one lap +// ends and the next begins - useful when reading four instances' output interleaved in one terminal. +const ( + messageHi = "hi" + messageStartingAgain = "starting again" +) + +// configDigest identifies the group of oracles talking to each other. Every instance must use the +// same one, and libocr keys an endpoint by it, so it is a constant rather than a setting: there is +// one group here and it is this app's. +var configDigest = ocr2types.ConfigDigest(sha256.Sum256([]byte("cre/pingpong/v1"))) + +// endpointLimits are generous for messages this small; they exist because libocr requires them. +var endpointLimits = ocr2types.BinaryNetworkEndpointLimits{ + MaxMessageLength: 1024, + MessagesRatePerOracle: 10, + MessagesCapacityPerOracle: 100, + BytesRatePerOracle: 10 * 1024, + BytesCapacityPerOracle: 100 * 1024, +} + +// pingPong passes a message round the ring of oracles over one binary network endpoint. +type pingPong struct { + services.Service + eng *services.Engine + + lggr logger.Logger + cfg *Config + factories *ocr.OCRFactories + + // me is this instance's position in cfg.Peers, which is also the oracle ID every other instance + // addresses it by. Resolved in start, since it depends on the peer ID the proxy hosts for us. + me commontypes.OracleID + // oracles is how many of us there are, from cfg.Peers. + oracles int + + endpoint commontypes.BinaryNetworkEndpoint + // lapDone carries the hi that came back round to oracle 0, telling it to start the next lap. + // Buffered so a lap completing while 0 is between waits is not lost. + lapDone chan struct{} +} + +var _ services.Service = (*pingPong)(nil) + +func newPingPong(lggr logger.Logger, cfg *Config, factories *ocr.OCRFactories) *pingPong { + p := &pingPong{lggr: lggr, cfg: cfg, factories: factories, lapDone: make(chan struct{}, 1)} + p.Service, p.eng = services.Config{ + Name: "PingPong", + Start: p.start, + Close: p.close, + }.NewServiceEngine(lggr) + return p +} + +func (p *pingPong) start(ctx context.Context) error { + peerIDs, locators, err := parsePeers(p.cfg.Peers) + if err != nil { + return err + } + // An oracle ID is a uint8, so a ring longer than that cannot address its own members. + if len(peerIDs) < 2 || len(peerIDs) > math.MaxUint8 { + return fmt.Errorf("--pingpong.peers needs between two and %d peers to pass a message between, got %d", math.MaxUint8, len(peerIDs)) + } + p.oracles = len(peerIDs) + + // Which oracle this instance is, is not configured: it is where the peer the proxy hosts for us + // appears in the ring. Being a position in a ring that was just bounded, it fits an oracle ID. + me := slices.Index(peerIDs, p.factories.PeerID.String()) + if me < 0 || me > math.MaxUint8 { + return fmt.Errorf("this instance's peer %s is not one of --pingpong.peers %v", p.factories.PeerID, peerIDs) + } + p.me = commontypes.OracleID(me) + + // Created through the proxy: the peer that dials, announces and encrypts is in the crecore + // process at --ocr.proxy-address, and what comes back here is a plain endpoint. The peers' + // addresses go with it, since that proxy has no other way of knowing where they are. + endpoint, err := p.factories.OCR2Endpoint.NewEndpoint(configDigest, peerIDs, locators, 1, endpointLimits) + if err != nil { + return fmt.Errorf("failed to create endpoint: %w", err) + } + if err := endpoint.Start(); err != nil { + return fmt.Errorf("failed to start endpoint: %w", err) + } + p.endpoint = endpoint + + p.lggr.Infow("Ping pong ready", "oracle", p.me, "peerID", p.factories.PeerID.String(), "oracles", p.oracles) + + p.eng.Go(p.receive) + if p.me == 0 { + p.eng.Go(p.lead) + } + return nil +} + +// receive prints every message that arrives and passes the lap on. +func (p *pingPong) receive(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case msg := <-p.endpoint.Receive(): + fmt.Printf("I am %d and got the message %s from %d\n", p.me, msg.Msg, msg.Sender) + p.forward(string(msg.Msg)) + } + } +} + +// forward continues the lap: everyone but the last says hi to the next oracle along, the last says +// hi back to oracle 0, and oracle 0 - whose hi can only have come from the last - takes that as the +// lap being complete. +func (p *pingPong) forward(message string) { + if message != messageHi { + return // the broadcast marking a new lap; oracle 0 is about to say hi anyway + } + + switch { + case p.me == 0: + select { + case p.lapDone <- struct{}{}: + default: // a lap is already waiting to be counted + } + case int(p.me) == p.oracles-1: + p.send(messageHi, 0) + default: + p.send(messageHi, p.me+1) + } +} + +// lead is oracle 0's job: start a lap, wait for it to come back, announce the next one and go again. +// It re-sends rather than waiting forever, because a hi sent before the peers have finished +// connecting is dropped like any other message to an unreachable peer, and one lost message would +// otherwise leave every instance waiting quietly. +func (p *pingPong) lead(ctx context.Context) { + if !sleep(ctx, p.cfg.StartDelay.Duration()) { + return + } + + retry := time.NewTimer(p.cfg.RetryDelay.Duration()) + defer retry.Stop() + + for { + p.send(messageHi, 1) + retry.Reset(p.cfg.RetryDelay.Duration()) + + select { + case <-ctx.Done(): + return + case <-p.lapDone: + p.lggr.Infow("Lap complete, starting again") + p.broadcast(messageStartingAgain) + if !sleep(ctx, p.cfg.RoundDelay.Duration()) { + return + } + case <-retry.C: + p.lggr.Infow("Lap did not come back round, starting it again") + } + } +} + +func (p *pingPong) send(message string, to commontypes.OracleID) { + p.lggr.Debugw("Sending", "message", message, "to", to) + p.endpoint.SendTo([]byte(message), to) +} + +func (p *pingPong) broadcast(message string) { + p.lggr.Debugw("Broadcasting", "message", message) + p.endpoint.Broadcast([]byte(message)) +} + +func (p *pingPong) close() error { + if p.endpoint != nil { + return p.endpoint.Close() + } + return nil +} + +// parsePeers splits peerID@host:port entries into the two things libocr wants them as: the peer IDs, +// in order, which is the oracle set and the ring; and the same peers as locators, which is how the +// hosting proxies are told where to find each other. +func parsePeers(peers []string) (peerIDs []string, locators []commontypes.BootstrapperLocator, err error) { + peerIDs = make([]string, 0, len(peers)) + locators = make([]commontypes.BootstrapperLocator, 0, len(peers)) + + for _, peer := range peers { + peerID, address, found := strings.Cut(peer, "@") + if !found || peerID == "" || address == "" { + return nil, nil, fmt.Errorf("invalid --pingpong.peers entry %q: expected peerID@host:port", peer) + } + peerIDs = append(peerIDs, peerID) + locators = append(locators, commontypes.BootstrapperLocator{PeerID: peerID, Addrs: []string{address}}) + } + return peerIDs, locators, nil +} + +// sleep waits for d, reporting false if the context was cancelled first. +func sleep(ctx context.Context, d time.Duration) bool { + timer := time.NewTimer(d) + defer timer.Stop() + select { + case <-ctx.Done(): + return false + case <-timer.C: + return true + } +} diff --git a/crecore/pingpong/setup/main.go b/crecore/pingpong/setup/main.go new file mode 100644 index 000000000..32a6cbbe2 --- /dev/null +++ b/crecore/pingpong/setup/main.go @@ -0,0 +1,96 @@ +// Command setup gives one crecore proxy a database to run against: a keystore holding a fresh P2P +// key, in the shape crecore expects to find a node's keystore in. +// +// crecore borrows its peer identity from the node whose database it shares, by decrypting the +// node's key ring (the legacy encrypted_key_rings table). There is no node here, so this writes +// that row itself. It prints the peer ID of the key it created and nothing else, so a script can +// capture it and tell the app which peer it is talking as. +// +// For local demos only. The key is generated here and encrypted with a password passed on the +// command line, which is fine for something whose whole purpose is to say hi to itself four times. +package main + +import ( + "context" + "database/sql" + "flag" + "fmt" + "log" + "time" + + "github.com/smartcontractkit/chainlink-common/keystore" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/models" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" +) + +func main() { + url := flag.String("url", "", "postgres url of the database this proxy will use") + password := flag.String("password", "", "password the key ring is encrypted with; the proxy's --ocr.keystore-password") + flag.Parse() + + if *url == "" || *password == "" { + log.Fatal("both -url and -password are required") + } + + peerID, err := setup(context.Background(), *url, *password) + if err != nil { + log.Fatal(err) + } + + // The only thing on stdout, so `peer_id=$(setup ...)` works. + fmt.Println(peerID) +} + +// setup writes a key ring holding one new P2P key, replacing whatever was there, and returns the +// peer ID of that key. +func setup(ctx context.Context, url, password string) (string, error) { + db, err := sqlutil.OpenDB(sqlutil.Config{URL: url}) + if err != nil { + return "", fmt.Errorf("failed to open database: %w", err) + } + defer func() { _ = db.Close() }() + + key, err := p2pkey.NewV2() + if err != nil { + return "", fmt.Errorf("failed to generate a P2P key: %w", err) + } + + ring := models.NewKeyRing() + ring.P2P[key.ID()] = key + // Fast parameters: this is a throwaway key on a local machine, and the default ones take long + // enough per instance to be irritating. + encrypted, err := ring.Encrypt(password, keystore.FastScryptParams) + if err != nil { + return "", fmt.Errorf("failed to encrypt the key ring: %w", err) + } + + if err := writeKeyRing(ctx, db, encrypted.EncryptedKeys); err != nil { + return "", err + } + return key.ID(), nil +} + +// writeKeyRing creates the keystore table if this database has never held one - a node would have +// migrated it in - and leaves exactly one key ring in it. +func writeKeyRing(ctx context.Context, db *sql.DB, encryptedKeys []byte) error { + const createTable = `CREATE TABLE IF NOT EXISTS encrypted_key_rings ( + encrypted_keys bytea NOT NULL, + updated_at timestamptz NOT NULL + )` + if _, err := db.ExecContext(ctx, createTable); err != nil { + return fmt.Errorf("failed to create encrypted_key_rings: %w", err) + } + + // Replaced rather than added to: crecore takes the first row it finds, so a second one would + // make which peer it is a matter of luck. + if _, err := db.ExecContext(ctx, `DELETE FROM encrypted_key_rings`); err != nil { + return fmt.Errorf("failed to clear encrypted_key_rings: %w", err) + } + if _, err := db.ExecContext(ctx, + `INSERT INTO encrypted_key_rings (encrypted_keys, updated_at) VALUES ($1, $2)`, + encryptedKeys, time.Now()); err != nil { + return fmt.Errorf("failed to write the key ring: %w", err) + } + return nil +} diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index b507381c8..78eb4fd2f 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -45,7 +45,7 @@ type proxyService struct { // CapabilitiesRegistry) has registered too, so they share one address instead of each opening a // listener of their own. grpcServer grpc.ServiceRegistrar - factories *ocr.OCRFactories + factories *ocr.RageFactories } var _ services.Service = (*proxyService)(nil) @@ -53,7 +53,7 @@ var _ services.Service = (*proxyService)(nil) // newProxyService builds the proxy service using the standard // services.Config/Engine pattern, so its lifecycle and health integrate with // the bootstrapper's aggregated health report. -func newProxyService(lggr logger.Logger, grpcServer grpc.ServiceRegistrar, factories *ocr.OCRFactories) *proxyService { +func newProxyService(lggr logger.Logger, grpcServer grpc.ServiceRegistrar, factories *ocr.RageFactories) *proxyService { s := &proxyService{lggr: lggr, grpcServer: grpcServer, factories: factories} s.Service, s.eng = services.Config{ Name: "P2PProxy", @@ -71,5 +71,15 @@ func (s *proxyService) start(context.Context) error { // The factories back both surfaces over the same rage connection and discoverer. creproxy.RegisterBinaryNetworkEndpointProxyServer(s.grpcServer, NewServer(s.factories.OCR2Endpoint, metrics)) creproxy.RegisterEndpoint2ProxyServer(s.grpcServer, NewEndpoint2Server(s.factories.OCR3_1Endpoint, metrics)) + + // Signing goes on the same surface, and for the same reason: this process + // holds the node's keys, so an oracle hosted elsewhere asks it to sign + // rather than being given one. + signer, err := newSignerServer(s.factories.OCR2) + if err != nil { + return fmt.Errorf("failed to serve signing on behalf of capabilities: %w", err) + } + creproxy.RegisterSignerServer(s.grpcServer, signer) + return nil } diff --git a/crecore/signer_server.go b/crecore/signer_server.go new file mode 100644 index 000000000..2facb8c98 --- /dev/null +++ b/crecore/signer_server.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "errors" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" +) + +// signerServer signs on behalf of oracles that hold no keys. +// +// It is the same trade as the endpoint proxies beside it: this process has the +// node's identity, so it lends out what that identity can do rather than the +// identity itself. A capability running in another process decides what to sign +// - it runs the protocol - and this signs it with the node's OCR key, which is +// the key the registry lists as one of the DON's signers. +type signerServer struct { + creproxy.UnimplementedSignerServer + + bundle ocr2key.KeyBundle +} + +var _ creproxy.SignerServer = (*signerServer)(nil) + +// newSignerServer refuses a bundle it cannot be verified against. +// +// A caller checks signatures itself, since verification needs no secret, and it +// has to check them the way they were made. Rather than let that be discovered +// one rejected signature at a time - which looks like a DON that will not agree +// rather than like a misconfiguration - a bundle of another chain type is +// refused here, where it is still a startup error. +func newSignerServer(bundle ocr2key.KeyBundle) (*signerServer, error) { + if bundle == nil { + return nil, errors.New("no OCR2 key bundle to sign with") + } + if ct := bundle.ChainType(); ct != corekeys.EVM { + return nil, fmt.Errorf("OCR2 key bundle %s is for %s; signing on behalf of a capability is only supported for %s keys", + bundle.ID(), ct, corekeys.EVM) + } + return &signerServer{bundle: bundle}, nil +} + +func (s *signerServer) Keys(context.Context, *creproxy.KeysRequest) (*creproxy.KeysReply, error) { + offchain := s.bundle.OffchainPublicKey() + config := s.bundle.ConfigEncryptionPublicKey() + + return &creproxy.KeysReply{ + OffchainPublicKey: offchain[:], + ConfigEncryptionPublicKey: config[:], + OnchainPublicKey: s.bundle.PublicKey(), + MaxSignatureLength: uint32(s.bundle.MaxSignatureLength()), //#nosec G115 - a signature length is small + }, nil +} + +func (s *signerServer) SignOffchain(_ context.Context, req *creproxy.SignOffchainRequest) (*creproxy.SignatureReply, error) { + signature, err := s.bundle.OffchainSign(req.GetMessage()) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &creproxy.SignatureReply{Signature: signature}, nil +} + +func (s *signerServer) ConfigDiffieHellman(_ context.Context, req *creproxy.ConfigDiffieHellmanRequest) (*creproxy.ConfigDiffieHellmanReply, error) { + var point [32]byte + if got := len(req.GetPoint()); got != len(point) { + return nil, status.Errorf(codes.InvalidArgument, "point is %d bytes, want %d", got, len(point)) + } + copy(point[:], req.GetPoint()) + + shared, err := s.bundle.ConfigDiffieHellman(point) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &creproxy.ConfigDiffieHellmanReply{SharedSecret: shared[:]}, nil +} + +func (s *signerServer) SignReport(_ context.Context, req *creproxy.SignReportRequest) (*creproxy.SignatureReply, error) { + var digest [32]byte + if got := len(req.GetConfigDigest()); got != len(digest) { + return nil, status.Errorf(codes.InvalidArgument, "config digest is %d bytes, want %d", got, len(digest)) + } + copy(digest[:], req.GetConfigDigest()) + + signature, err := s.bundle.Sign3(digest, req.GetSeqNr(), req.GetReport()) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &creproxy.SignatureReply{Signature: signature}, nil +} diff --git a/libs/go.mod b/libs/go.mod index c5d637fd8..3fac54347 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -184,7 +184,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect - github.com/smartcontractkit/chain-selectors v1.0.100 // indirect + github.com/smartcontractkit/chain-selectors v1.0.104 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect diff --git a/libs/go.sum b/libs/go.sum index 5df9da89f..d429e25ba 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -630,6 +630,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= +github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= diff --git a/libs/ocr/database.go b/libs/ocr/database.go new file mode 100644 index 000000000..e2194b4f0 --- /dev/null +++ b/libs/ocr/database.go @@ -0,0 +1,182 @@ +// Package ocr holds the pieces of an OCR oracle that are not a node's and not a +// chain's, so that a capability can run one wherever it is hosted. +package ocr + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// Database is the libocr database of an OCR-based capability, held in memory. +// +// Nothing it holds outlives the process on purpose. The config is a cache, and a +// miss is answered by the config tracker rather than being an error. The +// protocol states are progress within one config digest, which an oracle +// restarting rejoins from the current config anyway. The pending transmissions +// are reports waiting to be delivered, and a capability delivers them to +// whoever called it, in this process - so one lost with the process is a request +// whose caller is no longer waiting for it either. +// +// That last point is what makes this safe here and not in general: an OCR job +// that writes to a chain must survive a restart still owing that write, and uses +// a durable database for exactly that reason. +// +// Capabilities are the only thing that runs on this. In the node it backs the +// one oracle factory the standard capabilities delegate builds, and nothing +// else: the OCR2 plugins that write to chains each bring their own durable +// database. So this lives here rather than in chainlink-common - it is not a +// general-purpose OCR database, and holding it in memory is a statement about +// capabilities rather than about OCR. +type Database struct { + // name identifies this oracle in errors, since a capability can run more + // than one. + name string + lggr logger.SugaredLogger + config *ocrtypes.ContractConfig + states map[ocrtypes.ConfigDigest]*ocrtypes.PersistentState + pendingTransmissions map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission + protocolStates map[ocrtypes.ConfigDigest]map[string][]byte + + mu sync.Mutex +} + +// Both, deliberately: the states, config and pending transmissions are the +// database every protocol from OCR2 on keeps, and the protocol state is what +// OCR3 adds on top of it. Nothing here is particular to either. +var ( + _ ocrtypes.Database = &Database{} + _ ocr3types.Database = &Database{} +) + +// NewDatabase returns the database of the oracle named name. +func NewDatabase(name string, lggr logger.Logger) *Database { + return &Database{ + name: name, + lggr: logger.Sugared(logger.Named(lggr, "OracleMemoryDB")), + states: make(map[ocrtypes.ConfigDigest]*ocrtypes.PersistentState), + pendingTransmissions: make(map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission), + protocolStates: make(map[ocrtypes.ConfigDigest]map[string][]byte), + } +} + +func (d *Database) ReadState(ctx context.Context, cd ocrtypes.ConfigDigest) (*ocrtypes.PersistentState, error) { + d.mu.Lock() + defer d.mu.Unlock() + + ps, ok := d.states[cd] + if !ok { + return nil, fmt.Errorf("state not found for oracle %s, config digest %s", d.name, cd) + } + + return ps, nil +} + +func (d *Database) WriteState(ctx context.Context, cd ocrtypes.ConfigDigest, state ocrtypes.PersistentState) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.states[cd] = &state + return nil +} + +// ReadConfig returns nil, nil when there is no config yet: that is a cache miss +// rather than a failure, and the caller resolves it from the config tracker. +func (d *Database) ReadConfig(ctx context.Context) (*ocrtypes.ContractConfig, error) { + d.mu.Lock() + defer d.mu.Unlock() + + if d.config == nil { + return nil, nil + } + return d.config, nil +} + +func (d *Database) WriteConfig(ctx context.Context, c ocrtypes.ContractConfig) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.config = &c + d.lggr.Debugw("Wrote config", "oracle", d.name, "configDigest", c.ConfigDigest, "configCount", c.ConfigCount) + + return nil +} + +func (d *Database) StorePendingTransmission(ctx context.Context, t ocrtypes.ReportTimestamp, tx ocrtypes.PendingTransmission) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.pendingTransmissions[t] = tx + return nil +} + +func (d *Database) PendingTransmissionsWithConfigDigest(ctx context.Context, cd ocrtypes.ConfigDigest) (map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission, error) { + d.mu.Lock() + defer d.mu.Unlock() + + m := make(map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission) + for k, v := range d.pendingTransmissions { + if k.ConfigDigest == cd { + m[k] = v + } + } + + return m, nil +} + +func (d *Database) DeletePendingTransmission(ctx context.Context, t ocrtypes.ReportTimestamp) error { + d.mu.Lock() + defer d.mu.Unlock() + + delete(d.pendingTransmissions, t) + return nil +} + +func (d *Database) DeletePendingTransmissionsOlderThan(ctx context.Context, t time.Time) error { + d.mu.Lock() + defer d.mu.Unlock() + + for k, v := range d.pendingTransmissions { + if v.Time.Before(t) { + delete(d.pendingTransmissions, k) + } + } + + return nil +} + +// ReadProtocolState returns nil, nil for a key that was never written, which is +// how libocr asks whether there is any. +func (d *Database) ReadProtocolState(ctx context.Context, configDigest ocrtypes.ConfigDigest, key string) ([]byte, error) { + d.mu.Lock() + defer d.mu.Unlock() + + value, ok := d.protocolStates[configDigest][key] + if !ok { + return nil, nil + } + return value, nil +} + +// WriteProtocolState writes value, or deletes the key when value is nil. +func (d *Database) WriteProtocolState(ctx context.Context, configDigest ocrtypes.ConfigDigest, key string, value []byte) error { + d.mu.Lock() + defer d.mu.Unlock() + + if value == nil { + delete(d.protocolStates[configDigest], key) + return nil + } + + if d.protocolStates[configDigest] == nil { + d.protocolStates[configDigest] = make(map[string][]byte) + } + d.protocolStates[configDigest][key] = value + return nil +} diff --git a/libs/ocr/digest.go b/libs/ocr/digest.go new file mode 100644 index 000000000..98fba301b --- /dev/null +++ b/libs/ocr/digest.go @@ -0,0 +1,117 @@ +package ocr + +import ( + "crypto/sha256" + "encoding/binary" + "fmt" + "math" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" +) + +// ConfigDigest computes the digest identifying an OCR3 configuration held in a +// CapabilitiesRegistry contract. +// +// The contract stores the configuration without a digest, so whoever reads it +// computes this. What goes into it is what makes a configuration this one: the +// chain and address it was read from, so a configuration cannot be replayed +// against another registry; the DON, capability and OCR instance it belongs to, +// so two instances of one capability cannot be confused for each other; and the +// configuration itself. +// +// Every process running or serving one of these configurations has to agree on +// the digest, byte for byte, or their oracles will not talk to each other. That +// is why this is here rather than copied into each of them. +// +// The OCR3 in the name of the configuration is not a version this file is +// limited to by accident: the digest carries +// ConfigDigestPrefixKeystoneOCR3Capability in its first two bytes, which libocr +// treats as a domain separator, so a digest made here is only ever valid for a +// keystone capability's protocol instance. Everything else on the way past - +// the database, the keyrings, ContractConfig itself - is the shared +// offchainreporting2plus vocabulary and carries no version. +func ConfigDigest( + chainID uint64, + registryAddress string, + capabilityID string, + donID uint32, + ocrConfigKey string, + cfg *capabilitiespb.OCR3Config, +) (ocrtypes.ConfigDigest, error) { + buf := []byte{} + + // 1. Chain ID (8 bytes, big-endian) + var chainIDBytes [8]byte + binary.BigEndian.PutUint64(chainIDBytes[:], chainID) + buf = append(buf, chainIDBytes[:]...) + + // 2. Registry address (length-prefixed) + buf = appendLengthPrefixed(buf, []byte(registryAddress)) + + // 3. DON ID (4 bytes, big-endian) + var donIDBytes [4]byte + binary.BigEndian.PutUint32(donIDBytes[:], donID) + buf = append(buf, donIDBytes[:]...) + + // 4. Capability ID (length-prefixed) + buf = appendLengthPrefixed(buf, []byte(capabilityID)) + + // 5. OCR config key (length-prefixed) + buf = appendLengthPrefixed(buf, []byte(ocrConfigKey)) + + // 6. Config count (8 bytes, big-endian) + var configCountBytes [8]byte + binary.BigEndian.PutUint64(configCountBytes[:], cfg.ConfigCount) + buf = append(buf, configCountBytes[:]...) + + // 7. Number of signers (1 byte) + if len(cfg.Signers) > math.MaxUint8 { + return ocrtypes.ConfigDigest{}, fmt.Errorf("too many signers: %d > %d", len(cfg.Signers), math.MaxUint8) + } + buf = append(buf, uint8(len(cfg.Signers))) //#nosec G115 + + // 8. Each signer (length-prefixed) + for _, signer := range cfg.Signers { + buf = appendLengthPrefixed(buf, signer) + } + + // 9. Each transmitter (length-prefixed) + for _, transmitter := range cfg.Transmitters { + buf = appendLengthPrefixed(buf, transmitter) + } + + // 10. F (1 byte) + if cfg.F > math.MaxUint8 { + return ocrtypes.ConfigDigest{}, fmt.Errorf("f value too large: %d > %d", cfg.F, math.MaxUint8) + } + buf = append(buf, uint8(cfg.F)) //#nosec G115 + + // 11. Onchain config (length-prefixed) + buf = appendLengthPrefixed(buf, cfg.OnchainConfig) + + // 12. Offchain config version (8 bytes, big-endian) + var offchainVersionBytes [8]byte + binary.BigEndian.PutUint64(offchainVersionBytes[:], cfg.OffchainConfigVersion) + buf = append(buf, offchainVersionBytes[:]...) + + // 13. Offchain config (length-prefixed) + buf = appendLengthPrefixed(buf, cfg.OffchainConfig) + + // Hash and create digest with prefix in first 2 bytes + hash := sha256.Sum256(buf) + var digest ocrtypes.ConfigDigest + binary.BigEndian.PutUint16(digest[:2], uint16(ocrtypes.ConfigDigestPrefixKeystoneOCR3Capability)) + copy(digest[2:], hash[2:]) + + return digest, nil +} + +func appendLengthPrefixed(buf []byte, data []byte) []byte { + var lenBytes [4]byte + binary.BigEndian.PutUint32(lenBytes[:], uint32(len(data))) //#nosec G115 - data length will never exceed uint32 max in practice + buf = append(buf, lenBytes[:]...) + buf = append(buf, data...) + return buf +} diff --git a/libs/ocr/oracle.go b/libs/ocr/oracle.go new file mode 100644 index 000000000..75820bcae --- /dev/null +++ b/libs/ocr/oracle.go @@ -0,0 +1,159 @@ +package ocr + +import ( + "context" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/smartcontractkit/libocr/commontypes" + libocr "github.com/smartcontractkit/libocr/offchainreporting2plus" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3shims" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// OracleArgs is what a capability brings to an oracle of its own: what it is, +// what it computes, where it sends the result, and the identity it runs under. +// +// Everything else - the configuration, the digest, the database - follows from +// these, which is the point: a capability hosted outside a node states what it +// does and is handed an oracle, rather than assembling libocr itself. +type OracleArgs struct { + // CapabilityID and DonID say which configuration in the registry is this + // oracle's. Key names the OCR instance for a capability running more than + // one; empty is its only one. + CapabilityID string + DonID uint32 + Key string + + // Registry is where the configuration comes from, digest included. + Registry core.OCRConfigRegistry + + // Endpoints is the networking, and Keyrings the identity. Both come from + // whoever holds the node's peer - see libs/standalone/ocr. + Endpoints ocrtypes.BinaryNetworkEndpointFactory + Offchain ocrtypes.OffchainKeyring + Onchain ocr3types.OnchainKeyring[[]byte] + + // Bootstrappers are the peers to dial before this oracle has heard of + // anyone. The registry lists who the oracle set is, not where to find them. + Bootstrappers []commontypes.BootstrapperLocator + + Plugin ocr3types.ReportingPluginFactory[[]byte] + Transmitter ocr3types.ContractTransmitter[[]byte] + + LocalConfig ocrtypes.LocalConfig + Logger logger.Logger + Metrics prometheus.Registerer +} + +// NewOracle builds the libocr oracle a capability runs. +func NewOracle(args OracleArgs) (libocr.Oracle, error) { + if args.Registry == nil { + return nil, fmt.Errorf("capability %s has no registry to read its OCR config from", args.CapabilityID) + } + + tracker := ®istryTracker{ + registry: args.Registry, + capabilityID: args.CapabilityID, + donID: args.DonID, + key: args.Key, + } + + metrics := args.Metrics + if metrics == nil { + metrics = prometheus.NewRegistry() + } + + return libocr.NewOracle(libocr.OCR3OracleArgs2[[]byte]{ + BinaryNetworkEndpointFactory: args.Endpoints, + V2Bootstrappers: args.Bootstrappers, + ContractConfigTracker: tracker, + OffchainConfigDigester: digester{}, + ContractTransmitter: args.Transmitter, + ReportingPluginFactory: args.Plugin, + Database: NewDatabase(args.CapabilityID, args.Logger), + LocalConfig: args.LocalConfig, + Logger: logger.NewOCRWrapper(args.Logger, true, func(string) {}), + MetricsRegisterer: metrics, + MonitoringEndpoint: monitoringEndpoint{}, + OffchainKeyring: args.Offchain, + OnchainKeyring: ocr3shims.OnchainKeyringAsOnchainKeyring2(args.Onchain), + }) +} + +// registryTracker is the capabilities registry, as a libocr config tracker. +// +// Where an on-chain OCR job watches its contract, a capability's configuration +// is a record in the CapabilitiesRegistry, so this reads it. There is nothing to +// subscribe to - the registry is a snapshot someone else keeps fresh - so it +// notifies nothing and libocr polls it at LocalConfig.ContractConfigTrackerPollInterval. +type registryTracker struct { + registry core.OCRConfigRegistry + capabilityID string + donID uint32 + key string +} + +var _ ocrtypes.ContractConfigTracker = (*registryTracker)(nil) + +func (t *registryTracker) Notify() <-chan struct{} { return nil } + +func (t *registryTracker) LatestConfigDetails(ctx context.Context) (uint64, ocrtypes.ConfigDigest, error) { + config, err := t.config(ctx) + if err != nil { + return 0, ocrtypes.ConfigDigest{}, err + } + // Block zero always: a registry record has no block a caller can ask about, + // and libocr only uses this to tell one configuration from another, which + // the digest already does. + return 0, config.ConfigDigest, nil +} + +func (t *registryTracker) LatestConfig(ctx context.Context, _ uint64) (ocrtypes.ContractConfig, error) { + return t.config(ctx) +} + +func (t *registryTracker) LatestBlockHeight(context.Context) (uint64, error) { return 0, nil } + +func (t *registryTracker) config(ctx context.Context) (ocrtypes.ContractConfig, error) { + config, err := t.registry.OCRConfig(ctx, t.capabilityID, t.donID, t.key) + if err != nil { + return ocrtypes.ContractConfig{}, err + } + if config.ConfigDigest == (ocrtypes.ConfigDigest{}) { + return ocrtypes.ContractConfig{}, fmt.Errorf( + "the registry returned the OCR config of capability %s without a digest", t.capabilityID) + } + return config, nil +} + +// digester hands back the digest the configuration arrived with. +// +// Computing it is the registry's job, since it covers the chain and address the +// configuration was read from and a capability is not told either. Every oracle +// in the DON therefore agrees on the digest by having been given the same one, +// rather than by all computing it the same way. +type digester struct{} + +var _ ocrtypes.OffchainConfigDigester = digester{} + +func (digester) ConfigDigest(_ context.Context, config ocrtypes.ContractConfig) (ocrtypes.ConfigDigest, error) { + return config.ConfigDigest, nil +} + +func (digester) ConfigDigestPrefix(context.Context) (ocrtypes.ConfigDigestPrefix, error) { + return ocrtypes.ConfigDigestPrefixKeystoneOCR3Capability, nil +} + +// monitoringEndpoint drops libocr's telemetry. A capability reports through +// beholder like everything else in its process. +type monitoringEndpoint struct{} + +var _ commontypes.MonitoringEndpoint = monitoringEndpoint{} + +func (monitoringEndpoint) SendLog([]byte) {} diff --git a/libs/standalone/capability/capability.go b/libs/standalone/capability/capability.go index 48350bd61..c2866531e 100644 --- a/libs/standalone/capability/capability.go +++ b/libs/standalone/capability/capability.go @@ -1,14 +1,20 @@ package capability import ( - "context" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/services" ) +// Capability is what this package hosts: something that runs, and that can be +// called as a capability. +// +// There is no Initialise. A capability is handed what it needs when it is built +// - Run takes the Dependencies, so whatever constructs the capabilities passed +// to it has them too - and the rest of what an Initialise would have done is +// this package's own: registering the capability, serving it, announcing it, and +// taking it back out again. That leaves nothing to ask the capability to do +// between being built and being started, and so no method to ask it with. type Capability interface { services.Service - Initialise(ctx context.Context, dependencies *Dependencies) error - Info(ctx context.Context) (capabilities.CapabilityInfo, error) + capabilities.ExecutableAndTriggerCapability } diff --git a/libs/standalone/capability/dependency.go b/libs/standalone/capability/dependency.go index 603a22392..b879a9c88 100644 --- a/libs/standalone/capability/dependency.go +++ b/libs/standalone/capability/dependency.go @@ -62,6 +62,21 @@ type Dependencies struct { closers []func() error } +// OCRConfigRegistry is where an OCR-based capability reads the configuration it +// runs under: the node's registry, which computes the config digest from the +// contract it read - something a capability is deliberately not told. +// +// Nil when this process has no node behind it, as an embedded run does not: an +// oracle then has no configuration to join, which is a clearer failure than one +// invented locally that no other member of the DON agrees with. +func (d Dependencies) OCRConfigRegistry() core.OCRConfigRegistry { + registry, ok := d.proxy.(core.OCRConfigRegistry) + if !ok { + return nil + } + return registry +} + // Close releases whatever Get dialled. The bootstrapper closes resolved // dependency values on shutdown, after the services built from them. func (d Dependencies) Close() error { @@ -244,8 +259,7 @@ func reloadHandler(lggr logger.Logger, settings *loop.AtomicSettings, path strin } } -// registrar initialises each capability, makes it reachable, and takes it back -// out on shutdown. +// registrar makes each capability reachable, and takes it back out on shutdown. // // Reachable means three things, in order: the local registry holds it, so // anything else in this process resolves it as a value; a gRPC server of its own @@ -291,9 +305,6 @@ func newRegistrar(lggr logger.Logger, deps Dependencies, caps []Capability) *reg func (r *registrar) start(ctx context.Context) error { for i, c := range r.caps { - if err := c.Initialise(ctx, &r.deps); err != nil { - return fmt.Errorf("failed to initialise capability %d: %w", i, err) - } info, err := c.Info(ctx) if err != nil { return fmt.Errorf("failed to read capability %d info: %w", i, err) diff --git a/libs/standalone/grpc/dependency.go b/libs/standalone/grpc/dependency.go new file mode 100644 index 000000000..6d8ec6ae6 --- /dev/null +++ b/libs/standalone/grpc/dependency.go @@ -0,0 +1,159 @@ +package grpc + +import ( + "context" + "net" + "strconv" + "sync" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// namespace roots both configs, so every gRPC setting is a grpc.* one however +// many of these a binary takes. They use different keys, since two configs +// binding one key would leave the value read from whichever registered last. +const namespace = "grpc" + +// defaultHost is where a server binds and is advertised unless told otherwise. +// +// localhost rather than every interface: a process reached only by the node that +// launched it should not be listening publicly. Set the host to whatever this +// process is reachable at (a container name, a service DNS name, or empty for +// every interface) when something off-box has to dial it. +const defaultHost = "localhost" + +// Config is the configured server's address. +// +// Host is both what the server binds to and what it is advertised as: a service +// that hands its address to something else can only be reached where the server +// is actually listening, so keeping them one setting means the two cannot +// disagree. +type Config struct { + Host string `usage:"host the gRPC server binds to and is advertised at; empty binds every interface"` + Port uint16 `validate:"required" usage:"port the gRPC server listens on. Instance i of an embed run listens on this port plus i"` +} + +// Dependency returns the process's gRPC server, bound to the configured address. +// +// lggr names the server in the logs, and the caller names it rather than this +// does: a binary running one server calls it what that server is for. +func Dependency(lggr logger.Logger) standalone.BootstrapDependency[*Server] { + // Wrapped so the port is bound once however many services resolve this. + return standalone.OnceBootstrapper[*Server](&dependency{lggr: lggr, cfg: Config{Host: defaultHost}}) +} + +type dependency struct { + lggr logger.Logger + cfg Config + index int +} + +var _ standalone.BootstrapDependency[*Server] = (*dependency)(nil) + +func (d *dependency) Namespace() string { return namespace } + +func (d *dependency) Config() any { return &d.cfg } + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { return nil } + +// ForEmbedding gives instance i the configured port plus i, so instances sharing +// a process do not collide over it - the same rule the metrics/health server +// follows. The settings are otherwise the configured ones: an address is an +// address whether or not there are siblings. +func (d *dependency) ForEmbedding(i int) standalone.BootstrapDependency[*Server] { + return standalone.OnceBootstrapper[*Server](&dependency{lggr: d.lggr, cfg: d.cfg, index: i}) +} + +func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (*Server, error) { + port := d.cfg.Port + uint16(d.index) + return newServer(ctx, d.lggr, net.JoinHostPort(d.cfg.Host, strconv.Itoa(int(port)))) +} + +// FactoryConfig configures the servers the factory makes: where they are +// reachable, and optionally where their ports start. +type FactoryConfig struct { + AdvertiseHost string `usage:"host the gRPC servers this process opens are advertised at; empty binds every interface"` + + // StartPort is where the factory's ports begin. Zero, the default, means every + // server asks the OS for a free port instead - which is what a process + // announcing its own addresses wants, since nothing has to predict them. + // + // Set it when something outside the process does have to predict them: a + // firewall rule, a port mapping, or an operator reading a log. The ports are + // then consecutive from here, one per server, in the order they are opened. + StartPort uint16 `usage:"first port the gRPC servers this process opens bind to, incrementing per server; 0 asks the OS for a free port for each of them"` +} + +// Factory makes gRPC servers, one per thing that has to be told apart by address +// - see the package comment. Each call binds a port immediately, so the caller +// can announce the address before anything is serving on it. +// +// One factory hands out one run of ports, and it is shared rather than copied +// per embedded instance: instances live in one process and so compete for the +// same ports, and a counter each would have them all try to bind StartPort. +type Factory struct { + host string + startPort uint16 + + mu sync.Mutex + opened uint16 // servers made so far, which is the offset from startPort +} + +// New returns a server bound to the next port on the configured host. lggr names +// it, and the caller names it after whatever it serves, so a process with several +// says which is which. +func (f *Factory) New(ctx context.Context, lggr logger.Logger) (*Server, error) { + return newServer(ctx, lggr, net.JoinHostPort(f.host, strconv.Itoa(int(f.nextPort())))) +} + +// nextPort is the port the next server binds. +// +// Zero is not a port but a request for any free one, so it is handed out as-is +// however many servers ask: incrementing it would turn "any port" into a +// deliberate 1, 2, 3, which are neither free nor wanted. +func (f *Factory) nextPort() uint16 { + if f.startPort == 0 { + return 0 + } + f.mu.Lock() + defer f.mu.Unlock() + port := f.startPort + f.opened + f.opened++ + return port +} + +// FactoryDependency returns a factory for gRPC servers on ephemeral ports. +// +// lggr is what the factory itself logs under; each server New makes is named by +// whoever asks for it. +func FactoryDependency(lggr logger.Logger) standalone.BootstrapDependency[*Factory] { + return standalone.OnceBootstrapper[*Factory](&factoryDependency{lggr: lggr, cfg: FactoryConfig{AdvertiseHost: defaultHost}}) +} + +type factoryDependency struct { + lggr logger.Logger + cfg FactoryConfig +} + +var _ standalone.BootstrapDependency[*Factory] = (*factoryDependency)(nil) + +func (d *factoryDependency) Namespace() string { return namespace } + +func (d *factoryDependency) Config() any { return &d.cfg } + +func (d *factoryDependency) Dependencies() []standalone.BootstrapCommand { return nil } + +// ForEmbedding returns the receiver, so every instance resolves the one factory +// and draws from the one run of ports. +// +// This is the case BootstrapDependency.ForEmbedding describes as a dependency +// backed by a process-wide resource: ports are the process's, not an instance's. +// Partitioning them per instance the way the configured server does would need a +// stride - how many servers an instance opens - that only the instance knows, and +// a shared counter needs no such guess. +func (d *factoryDependency) ForEmbedding(_ int) standalone.BootstrapDependency[*Factory] { return d } + +func (d *factoryDependency) Get(_ context.Context, _ standalone.CommonConfig) (*Factory, error) { + return &Factory{host: d.cfg.AdvertiseHost, startPort: d.cfg.StartPort}, nil +} diff --git a/libs/standalone/grpc/dependency_test.go b/libs/standalone/grpc/dependency_test.go new file mode 100644 index 000000000..bc65f01bf --- /dev/null +++ b/libs/standalone/grpc/dependency_test.go @@ -0,0 +1,117 @@ +package grpc + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +func TestDependency_BindsConfiguredPort(t *testing.T) { + d := &dependency{lggr: logger.Test(t), cfg: Config{Host: defaultHost, Port: freePort(t)}} + + srv, err := d.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, srv.Close()) }) + + assert.Equal(t, net.JoinHostPort("127.0.0.1", srv.Port()), srv.Address()) + assert.Equal(t, uint16Port(t, srv), d.cfg.Port, "should bind the configured port, not an ephemeral one") +} + +// Embedding partitions the port the same way the metrics server does, so +// instances sharing a process do not collide. +func TestDependency_ForEmbeddingOffsetsPort(t *testing.T) { + base := freePort(t) + d := &dependency{lggr: logger.Test(t), cfg: Config{Host: defaultHost, Port: base}} + + srv, err := d.ForEmbedding(2).Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, srv.Close()) }) + + assert.Equal(t, base+2, uint16Port(t, srv)) +} + +func TestFactory_BindsEphemeralPortAndReportsIt(t *testing.T) { + fd := &factoryDependency{lggr: logger.Test(t), cfg: FactoryConfig{AdvertiseHost: defaultHost}} + f, err := fd.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + + first, err := f.New(t.Context(), logger.Test(t)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, first.Close()) }) + + second, err := f.New(t.Context(), logger.Test(t)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, second.Close()) }) + + // Port 0 is what it asks for; the address it reports is the one it got, which + // is the whole point - a caller announcing itself cannot advertise 0. + assert.NotEqual(t, "0", first.Port()) + assert.NotEqual(t, first.Address(), second.Address(), "each server gets its own port") + assert.Equal(t, net.JoinHostPort("127.0.0.1", first.Port()), first.Address()) +} + +func TestFactory_StartPortIncrementsPerServer(t *testing.T) { + start := freePort(t) + fd := &factoryDependency{lggr: logger.Test(t), cfg: FactoryConfig{AdvertiseHost: defaultHost, StartPort: start}} + f, err := fd.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + + for i := range uint16(3) { + srv, err := f.New(t.Context(), logger.Test(t)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, srv.Close()) }) + assert.Equal(t, start+i, uint16Port(t, srv)) + } +} + +// The factory is shared across an embed run, so its ports keep running on rather +// than every instance starting over at StartPort and colliding. +func TestFactory_SharedAcrossEmbeddedInstances(t *testing.T) { + start := freePort(t) + // Built the way FactoryDependency builds it, with the config set to what the + // flags would have decoded into it. + dep := standalone.OnceBootstrapper[*Factory](&factoryDependency{ + lggr: logger.Test(t), + cfg: FactoryConfig{AdvertiseHost: defaultHost, StartPort: start}, + }) + + first, err := dep.ForEmbedding(0).Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + second, err := dep.ForEmbedding(1).Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + assert.Same(t, first, second, "every instance should draw from the one factory") + + a, err := first.New(t.Context(), logger.Test(t)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, a.Close()) }) + b, err := second.New(t.Context(), logger.Test(t)) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, b.Close()) }) + + assert.Equal(t, start, uint16Port(t, a)) + assert.Equal(t, start+1, uint16Port(t, b), "the second instance continues the run rather than restarting it") +} + +// uint16Port is srv's bound port, for comparing against a configured one. +func uint16Port(t *testing.T, srv *Server) uint16 { + t.Helper() + addr, ok := srv.listener.Addr().(*net.TCPAddr) + require.True(t, ok) + return uint16(addr.Port) +} + +// freePort asks the OS for a port and hands it back, so the test configures a +// port that is actually available. +func freePort(t *testing.T) uint16 { + t.Helper() + l, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + port := uint16(l.Addr().(*net.TCPAddr).Port) + require.NoError(t, l.Close()) + return port +} diff --git a/libs/standalone/grpc/server.go b/libs/standalone/grpc/server.go new file mode 100644 index 000000000..4e52ccc2e --- /dev/null +++ b/libs/standalone/grpc/server.go @@ -0,0 +1,125 @@ +// Package grpc provides the gRPC servers a standalone CRE binary serves on. +// +// There are two dependencies here, and which one a binary takes says what its +// address means: +// +// - Dependency: one server at a configured address (--grpc.host, --grpc.port). +// For a process something else is told to dial - crecore, whose registry and +// proxy services core reaches at exactly that address, so it cannot be +// ephemeral. +// - FactoryDependency: a factory making servers on ephemeral ports. For a +// process that announces its own addresses rather than being told one, which +// is what a capability host does: it registers each capability with the +// address serving it, so the address only has to be knowable, not fixed. +// +// The factory exists because one address cannot serve two capabilities. A +// registry handle is an ID, a type and a callback URL, and of the RPCs reached +// through that URL only Execute carries a capability ID - BaseCapability.Info +// takes an Empty, and the registration calls carry workflow or trigger metadata. +// So the address is what identifies the capability, and a binary hosting several +// needs one server per capability. That is what the LOOP transport does too: it +// serves each capability on its own grpc.Server behind its own go-plugin broker +// connection, and this is the same arrangement without the broker. +package grpc + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "sync/atomic" + + "google.golang.org/grpc" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// Server is one gRPC server: bound when it is created, served for as long as it +// runs. +// +// Binding early is what makes an ephemeral port usable. A server on port 0 has no +// address until something listens, and a caller that has to announce its address +// needs it before anything is serving - so the listener is opened by the +// constructor and only Serve waits for Start. It also means a port already in use +// fails while the process is still starting up rather than once it is nominally +// running. +type Server struct { + services.Service + eng *services.Engine + + server *grpc.Server + listener net.Listener + started atomic.Bool +} + +// newServer binds address and returns a server for it. address is host:port; port +// 0 asks the OS for a free one, which is logged once bound. +func newServer(ctx context.Context, lggr logger.Logger, address string) (*Server, error) { + var lc net.ListenConfig + listener, err := lc.Listen(ctx, "tcp", address) + if err != nil { + return nil, fmt.Errorf("failed to listen on %s: %w", address, err) + } + + s := &Server{server: grpc.NewServer(), listener: listener} + s.Service, s.eng = services.Config{ + Name: "GRPCServer", + Start: s.start, + Close: s.close, + }.NewServiceEngine(lggr) + + s.eng.Infow(fmt.Sprintf("Bound gRPC to port %s", s.Port()), "address", s.Address()) + return s, nil +} + +// Registrar is where services register their RPCs. They must all have done so +// before the server starts: Serve does not accept registrations. +func (s *Server) Registrar() grpc.ServiceRegistrar { return s.server } + +// Address is the grpc.NewClient target for this server, which is what a caller +// announcing itself hands out. It is the address as bound, so a server on port 0 +// reports the port it actually got. +func (s *Server) Address() string { return s.listener.Addr().String() } + +// Port is the port this server bound, as a string. +func (s *Server) Port() string { + if _, port, err := net.SplitHostPort(s.Address()); err == nil { + return port + } + if tcp, ok := s.listener.Addr().(*net.TCPAddr); ok { + return strconv.Itoa(tcp.Port) + } + return "unknown" +} + +func (s *Server) start(context.Context) error { + s.started.Store(true) + s.eng.Go(func(context.Context) { + if err := s.server.Serve(s.listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) { + s.eng.Errorw("gRPC server stopped", "err", err) + } + }) + return nil +} + +// Close releases the port, whether or not the server ever served on it. +// +// The two cases differ because the listener is opened by the constructor: a +// server that was started is stopped through the service engine, which closes it +// as part of stopping; one that was not has no engine state to unwind and would +// otherwise hold the port until the process exits - which is exactly the case a +// caller hits when it binds a server and then fails before starting it. +func (s *Server) Close() error { + if s.started.Load() { + return s.Service.Close() + } + s.server.Stop() + return s.listener.Close() +} + +func (s *Server) close() error { + s.server.GracefulStop() + return nil +} diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index 9148c0c12..0f3aabeb5 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -31,6 +31,7 @@ import ( ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" ) @@ -51,6 +52,10 @@ type OCRFactories struct { // registry metadata must know which node it is. PeerID ragetypes.PeerID + // Keyrings sign this oracle's protocol messages and its reports. Resolved with the networking + // because they come from the same place: whoever holds the node's identity holds both. + Keyrings + closer io.Closer } @@ -73,6 +78,11 @@ type RageFactories struct { // Keyring signs with the same P2P key the peer above uses, for don2don.Dispatcher's // message-level signatures. Keyring ragetypes.PeerKeyring + + // OCR2 is the node's OCR key bundle, which this process signs with on behalf of oracles that + // hold no keys of their own. Nil for an embedded run, which has no node keystore to take one + // from. + OCR2 ocr2key.KeyBundle } // defaultPeerConfig is the peer settings the flags are bound to and decoded into, so an unset diff --git a/libs/standalone/ocr/host.go b/libs/standalone/ocr/host.go index 37da0c317..ab2f9e978 100644 --- a/libs/standalone/ocr/host.go +++ b/libs/standalone/ocr/host.go @@ -40,6 +40,10 @@ type Config struct { // KeystorePassword unlocks the keystore holding the P2P identity the peer announces under. // Typed as a SecretString so it redacts itself in logs, docs and generated example configs. KeystorePassword config.SecretString `usage:"password for the node keystore holding the shared P2P identity; required unless the identity is derived, as it is under embed"` + + // KeyBundleID names the OCR2 key bundle to sign with on behalf of the capabilities this + // process signs for. Only needed when the node holds more than one. + KeyBundleID string `usage:"OCR2 key bundle in the node keystore to sign with; only needed when the node holds more than one"` } // Host returns a standalone.BootstrapDependency that resolves the libocr Factories from a peer @@ -95,7 +99,24 @@ func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (* if err != nil { return nil, err } - return d.localFactories(ds, keyring, peerID) + + // From the same key ring, unlocked once: this process is the only one given the password, so + // it is the only one that can sign as this node - with its P2P key on the wire, and with its + // OCR keys for whoever it signs on behalf of. + bundle, err := loadOCR2Bundle(ctx, ds, string(d.cfg.KeystorePassword), d.cfg.KeyBundleID) + if err != nil { + return nil, err + } + + factories, err := d.localFactories(ds, keyring, peerID) + if err != nil { + return nil, err + } + factories.OCR2 = bundle + // Held here, so an oracle in this process signs with the same bundle this serves to oracles + // elsewhere rather than going out over gRPC to reach a key it already has. + factories.Keyrings = Keyrings{Offchain: localKeyring{bundle}, Onchain: localKeyring{bundle}} + return factories, nil } // keystoreIdentity loads the node's P2P identity from the keystore in the database this process diff --git a/libs/standalone/ocr/keyring.go b/libs/standalone/ocr/keyring.go index 494310745..6e6331e27 100644 --- a/libs/standalone/ocr/keyring.go +++ b/libs/standalone/ocr/keyring.go @@ -8,13 +8,16 @@ import ( "crypto/sha256" "errors" "fmt" + "sort" "strconv" + "strings" "github.com/jmoiron/sqlx" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/models" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" ) // loadPeerKeyring loads the P2P key from the node's keystore so this process @@ -50,6 +53,50 @@ func loadPeerKeyring(ctx context.Context, ds *sqlx.DB, password string) (*peerKe return nil, errors.New("no P2P key found in node key ring") } +// loadOCR2Bundle loads the node's OCR2 key bundle from the same key ring the P2P +// key came from, so the oracle signing done on a capability's behalf is done +// with the node's own OCR identity - the one the registry lists as a signer. +// +// bundleID names which bundle when the node has several. A node running one +// capability DON usually has one, and naming it then would be one more thing to +// keep in step with the keystore, so an empty bundleID takes the only one there +// is and refuses to guess between several. +func loadOCR2Bundle(ctx context.Context, ds *sqlx.DB, password, bundleID string) (ocr2key.KeyBundle, error) { + var encrypted []byte + if err := ds.GetContext(ctx, &encrypted, "SELECT encrypted_keys FROM encrypted_key_rings LIMIT 1"); err != nil { + return nil, fmt.Errorf("failed to read node key ring: %w", err) + } + kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(password) + if err != nil { + return nil, fmt.Errorf("failed to decrypt node key ring: %w", err) + } + + if bundleID != "" { + bundle, ok := kr.OCR2[bundleID] + if !ok { + return nil, fmt.Errorf("no OCR2 key bundle %q in the node key ring", bundleID) + } + return bundle, nil + } + + switch len(kr.OCR2) { + case 0: + return nil, errors.New("no OCR2 key bundle found in the node key ring") + case 1: + for _, bundle := range kr.OCR2 { + return bundle, nil + } + } + + ids := make([]string, 0, len(kr.OCR2)) + for id := range kr.OCR2 { + ids = append(ids, id) + } + sort.Strings(ids) + return nil, fmt.Errorf("the node key ring holds %d OCR2 key bundles (%s), so --ocr.key-bundle-id must say which one to sign with", + len(ids), strings.Join(ids, ", ")) +} + // deterministicSeedPrefix domain-separates the derived instance keys, so the seeds are specific // to this framework's embed mode and cannot collide with another scheme deriving keys from an // index. Changing it changes every derived peer ID. diff --git a/libs/standalone/ocr/proxy.go b/libs/standalone/ocr/proxy.go index baf5d732f..bee1f888b 100644 --- a/libs/standalone/ocr/proxy.go +++ b/libs/standalone/ocr/proxy.go @@ -7,7 +7,11 @@ import ( ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" @@ -76,7 +80,7 @@ func (d *proxyDependency) ForEmbedding(i int) standalone.BootstrapDependency[*OC return embeddedOCR{&embedded{lggr: d.lggr, index: i}} } -func (d *proxyDependency) Get(context.Context, standalone.CommonConfig) (*OCRFactories, error) { +func (d *proxyDependency) Get(ctx context.Context, _ standalone.CommonConfig) (*OCRFactories, error) { if d.cfg.ProxyAddress == "" { return nil, errors.New("--ocr.proxy-address is required to delegate rage networking") } @@ -97,12 +101,30 @@ func (d *proxyDependency) Get(context.Context, standalone.CommonConfig) (*OCRFac return nil, fmt.Errorf("failed to create proxy OCR3.1 endpoint factory: %w", err) } + // Signing is delegated to the same process, over a connection of its own: the keys are the + // node's, and the process hosting its peer is the one holding them. + conn, err := grpc.NewClient(d.cfg.ProxyAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + _ = endpointFactory.Close() + _ = endpoint2Factory.Close() + return nil, fmt.Errorf("failed to create signer client for %s: %w", d.cfg.ProxyAddress, err) + } + + keyring, err := newRemoteKeyring(ctx, conn) + if err != nil { + _ = endpointFactory.Close() + _ = endpoint2Factory.Close() + _ = conn.Close() + return nil, err + } + d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.cfg.ProxyAddress, "peerID", peerID.String()) return &OCRFactories{ OCR2Endpoint: endpointFactory, OCR3_1Endpoint: endpoint2Factory, PeerID: peerID, - closer: multiCloser{endpointFactory, endpoint2Factory}, + Keyrings: Keyrings{Offchain: keyring, Onchain: keyring}, + closer: multiCloser{endpointFactory, endpoint2Factory, conn}, }, nil } diff --git a/libs/standalone/ocr/signer.go b/libs/standalone/ocr/signer.go new file mode 100644 index 000000000..8441ce469 --- /dev/null +++ b/libs/standalone/ocr/signer.go @@ -0,0 +1,207 @@ +package ocr + +import ( + "context" + "fmt" + + "golang.org/x/crypto/curve25519" + "google.golang.org/grpc" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + + "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +// Keyrings are what an oracle signs with: its protocol messages, and its +// reports. +// +// They are resolved alongside the networking because they come from the same +// place. A process hosting the node's peer holds the node's keys; one delegating +// to it holds neither, and signs by asking - see remoteKeyring, and the Signer +// service the host serves. +type Keyrings struct { + Offchain ocrtypes.OffchainKeyring + Onchain ocr3types.OnchainKeyring[[]byte] +} + +// remoteKeyring signs by asking the process that holds the key. +// +// It is both keyrings at once because they are one key bundle on the other side, +// and splitting them here would only mean fetching the same public keys twice. +// +// The public halves are read once, when this is built: an oracle needs them to +// say who it is, they cannot change while it runs, and asking per call would put +// a round trip on operations that are pure local arithmetic everywhere else. +type remoteKeyring struct { + client proxy.SignerClient + + offchainPublicKey ocrtypes.OffchainPublicKey + configPublicKey ocrtypes.ConfigEncryptionPublicKey + onchainPublicKey ocrtypes.OnchainPublicKey + maxSignatureLen int +} + +var ( + _ ocrtypes.OffchainKeyring = (*remoteKeyring)(nil) + _ ocr3types.OnchainKeyring[[]byte] = (*remoteKeyring)(nil) +) + +// newRemoteKeyring dials the Signer service on conn and reads its public keys. +func newRemoteKeyring(ctx context.Context, conn grpc.ClientConnInterface) (*remoteKeyring, error) { + client := proxy.NewSignerClient(conn) + + keys, err := client.Keys(ctx, &proxy.KeysRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to read the signer's public keys: %w", err) + } + + k := &remoteKeyring{ + client: client, + onchainPublicKey: keys.GetOnchainPublicKey(), + maxSignatureLen: int(keys.GetMaxSignatureLength()), + } + + // Both are fixed-size, and a wrong length here would otherwise surface as an + // oracle no one recognises rather than as a bad reply. + if got := len(keys.GetOffchainPublicKey()); got != len(k.offchainPublicKey) { + return nil, fmt.Errorf("signer returned a %d byte offchain public key, want %d", got, len(k.offchainPublicKey)) + } + copy(k.offchainPublicKey[:], keys.GetOffchainPublicKey()) + + if got := len(keys.GetConfigEncryptionPublicKey()); got != len(k.configPublicKey) { + return nil, fmt.Errorf("signer returned a %d byte config encryption public key, want %d", got, len(k.configPublicKey)) + } + copy(k.configPublicKey[:], keys.GetConfigEncryptionPublicKey()) + + return k, nil +} + +func (k *remoteKeyring) OffchainSign(msg []byte) ([]byte, error) { + reply, err := k.client.SignOffchain(context.Background(), &proxy.SignOffchainRequest{Message: msg}) + if err != nil { + return nil, fmt.Errorf("failed to sign a protocol message: %w", err) + } + return reply.GetSignature(), nil +} + +func (k *remoteKeyring) ConfigDiffieHellman(point [curve25519.PointSize]byte) ([curve25519.PointSize]byte, error) { + var shared [curve25519.PointSize]byte + + reply, err := k.client.ConfigDiffieHellman(context.Background(), &proxy.ConfigDiffieHellmanRequest{Point: point[:]}) + if err != nil { + return shared, fmt.Errorf("failed to compute a config shared secret: %w", err) + } + if got := len(reply.GetSharedSecret()); got != len(shared) { + return shared, fmt.Errorf("signer returned a %d byte shared secret, want %d", got, len(shared)) + } + + copy(shared[:], reply.GetSharedSecret()) + return shared, nil +} + +func (k *remoteKeyring) OffchainPublicKey() ocrtypes.OffchainPublicKey { + return k.offchainPublicKey +} + +func (k *remoteKeyring) ConfigEncryptionPublicKey() ocrtypes.ConfigEncryptionPublicKey { + return k.configPublicKey +} + +func (k *remoteKeyring) PublicKey() ocrtypes.OnchainPublicKey { + return k.onchainPublicKey +} + +func (k *remoteKeyring) Sign(digest ocrtypes.ConfigDigest, seqNr uint64, report ocr3types.ReportWithInfo[[]byte]) ([]byte, error) { + reply, err := k.client.SignReport(context.Background(), &proxy.SignReportRequest{ + ConfigDigest: digest[:], + SeqNr: seqNr, + Report: report.Report, + }) + if err != nil { + return nil, fmt.Errorf("failed to sign a report: %w", err) + } + return reply.GetSignature(), nil +} + +// Verify is done here rather than asked for. It takes the signer's public key as +// an argument, so it needs no secret, and every signature an oracle receives +// would otherwise cost a round trip. +func (k *remoteKeyring) Verify( + publicKey ocrtypes.OnchainPublicKey, + digest ocrtypes.ConfigDigest, + seqNr uint64, + report ocr3types.ReportWithInfo[[]byte], + signature []byte, +) bool { + return verifyReport(publicKey, digest, seqNr, report.Report, signature) +} + +func (k *remoteKeyring) MaxSignatureLength() int { return k.maxSignatureLen } + +// verifyReport checks a signature the way an EVM key bundle makes one: the +// signed blob is the report bound to the round it belongs to, and the signature +// is secp256k1 over it. +// +// Only EVM, deliberately. A signature has to be verified against the scheme the +// signer used, and guessing wrong would accept nothing rather than accept the +// wrong thing - but it would do so silently, so the process holding the key +// refuses to serve a bundle of another chain type instead of letting this find +// out one signature at a time. +func verifyReport( + publicKey ocrtypes.OnchainPublicKey, + digest ocrtypes.ConfigDigest, + seqNr uint64, + report ocrtypes.Report, + signature []byte, +) bool { + return ocr2key.EvmVerifyBlob(publicKey, ocr2key.ReportToSigData3(digest, seqNr, report), signature) +} + +// localKeyring is a node key bundle used in the process that holds it. +// +// The bundle is an OCR2 keyring - it signs a report without knowing what a +// sequence number is - so this is the same adaptation the node makes when it +// runs an OCR3 capability itself: bind the report to its round, then sign that. +type localKeyring struct { + bundle ocr2key.KeyBundle +} + +var ( + _ ocrtypes.OffchainKeyring = localKeyring{} + _ ocr3types.OnchainKeyring[[]byte] = localKeyring{} +) + +func (k localKeyring) OffchainSign(msg []byte) ([]byte, error) { return k.bundle.OffchainSign(msg) } + +func (k localKeyring) ConfigDiffieHellman(point [curve25519.PointSize]byte) ([curve25519.PointSize]byte, error) { + return k.bundle.ConfigDiffieHellman(point) +} + +func (k localKeyring) OffchainPublicKey() ocrtypes.OffchainPublicKey { + return k.bundle.OffchainPublicKey() +} + +func (k localKeyring) ConfigEncryptionPublicKey() ocrtypes.ConfigEncryptionPublicKey { + return k.bundle.ConfigEncryptionPublicKey() +} + +func (k localKeyring) PublicKey() ocrtypes.OnchainPublicKey { return k.bundle.PublicKey() } + +func (k localKeyring) Sign(digest ocrtypes.ConfigDigest, seqNr uint64, report ocr3types.ReportWithInfo[[]byte]) ([]byte, error) { + return k.bundle.Sign3(digest, seqNr, report.Report) +} + +func (k localKeyring) Verify( + publicKey ocrtypes.OnchainPublicKey, + digest ocrtypes.ConfigDigest, + seqNr uint64, + report ocr3types.ReportWithInfo[[]byte], + signature []byte, +) bool { + return k.bundle.Verify3(publicKey, digest, seqNr, report.Report, signature) +} + +func (k localKeyring) MaxSignatureLength() int { return k.bundle.MaxSignatureLength() } diff --git a/libs/standalone/protohelpers/generator/downloader.go b/libs/standalone/protohelpers/generator/downloader.go new file mode 100644 index 000000000..93db4387a --- /dev/null +++ b/libs/standalone/protohelpers/generator/downloader.go @@ -0,0 +1,489 @@ +package generator + +import ( + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "sort" + "strings" +) + +const ( + protoVendor = ".proto_vendor" + dirPerm = 0700 +) + +// pseudoVersion matches the "--" suffix every Go +// pseudo-version ends with. The capture group is the commit to check out. +var pseudoVersion = regexp.MustCompile(`-[0-9]{14}-([0-9a-f]{12})$`) + +// downloader vendors the .proto sources of Go module dependencies. +// +// protoc needs every transitive import on disk, but a module such as +// github.com/smartcontractkit/chainlink-protos/cre/go only publishes generated +// Go code: the .proto files it was generated from live above the module root in +// the repository, so they never reach the module cache. download resolves each +// requested module against the caller's go.mod, copies the requested paths into +// .proto_vendor, and returns the directories to hand protoc as -I. +type downloader struct { + // Items maps a Go module path to the paths, relative to that module's root, + // that hold the .proto files to vendor. A path may point above the module + // root (for example "../") for modules whose .proto files sit outside the + // Go module; doing so forces a clone, since the module cache only holds the + // module's own subtree. + Items map[string][]string + + // Dir is the directory whose go.mod declares the modules in Items. Empty + // means the current working directory. + Dir string + + // Vendor is the directory the .proto_vendor cache is kept in, defaulting to + // Dir. Copies are keyed by module and version, so pointing several modules + // at one directory - the root of the repository they share, say - fetches a + // given release of a dependency once rather than once per module. + Vendor string +} + +func (d *downloader) vendorDir() string { + if d.Vendor == "" { + return filepath.Join(d.Dir, protoVendor) + } + return filepath.Join(d.Vendor, protoVendor) +} + +// vendored is one configured path of one module, after vendoring. +type vendored struct { + // Module is the module path the path was configured under. + Module string + + // Path is the path as configured, relative to the module root. + Path string + + // Dir is where the vendored copy of Path is on disk. It is a file rather + // than a directory when Path named a single .proto. + Dir string + + // Include is the directory protoc has to be given as -I for imports + // written against Path to resolve. Several paths of one module commonly + // share it; see Includes. + Include string +} + +// download vendors every configured module and returns one entry per requested +// path, in the order the paths were configured. +func (d *downloader) download() ([]vendored, error) { + if len(d.Items) == 0 { + return nil, nil + } + + if err := os.MkdirAll(d.vendorDir(), dirPerm); err != nil { + return nil, err + } + + // Iterate in a stable order so the returned include list, and therefore any + // protoc invocation built from it, does not change between runs. + repos := make([]string, 0, len(d.Items)) + for repo := range d.Items { + repos = append(repos, repo) + } + sort.Strings(repos) + + var all []vendored + for _, repo := range repos { + repoVendored, err := d.downloadModule(repo, d.Items[repo]) + if err != nil { + return nil, fmt.Errorf("%s: %w", repo, err) + } + all = append(all, repoVendored...) + } + + return all, nil +} + +func (d *downloader) downloadModule(repo string, want []string) ([]vendored, error) { + mod, err := resolve(d.Dir, repo) + if err != nil { + return nil, err + } + + // A local replace is already a full checkout of the repository, so the + // paths are usable in place. Vendoring one would only add a stale copy that + // shadows the edits the replace exists to pick up. + if mod.localReplace() { + base := filepath.ToSlash(mod.Dir) + found := make([]vendored, 0, len(want)) + for _, p := range want { + found = append(found, vendored{ + Module: repo, + Path: p, + Dir: filepath.Join(mod.Dir, filepath.FromSlash(p)), + Include: anchor(base, p), + }) + } + return found, nil + } + + // Key the vendor directory by version so bumping the dependency can never + // be served out of a copy made for the previous one. + vendorDir := filepath.Join(d.vendorDir(), escapePath(mod.Path)+"@"+mod.Version) + + // The module cache holds the module's subtree only, so anything reaching + // above the module root has to come from a clone. So does anything the + // cache is simply missing, which is the common case for .proto files that + // are not part of the published Go package. + subdir := "" + needClone := escapesRoot(want) || !existsUnder(mod.Dir, want) + if needClone { + if _, subdir, err = splitRepo(mod.Path); err != nil { + return nil, err + } + } + + // Resolve every requested path to a repository-relative path first: it is + // both the layout under vendorDir and, once we have a source tree, the path + // to copy from. Doing it up front means the vendor cache can be checked + // without paying for a clone. + rels := make([]string, 0, len(want)) + dirs := make([]string, 0, len(want)) + found := make([]vendored, 0, len(want)) + for _, p := range want { + rel := path.Join(subdir, p) + if rel == ".." || strings.HasPrefix(rel, "../") { + return nil, fmt.Errorf("path %q escapes the repository root", p) + } + dir := filepath.Join(vendorDir, filepath.FromSlash(rel)) + rels = append(rels, rel) + dirs = append(dirs, dir) + found = append(found, vendored{ + Module: repo, + Path: p, + Dir: dir, + Include: anchor(filepath.ToSlash(vendorDir)+"/"+subdir, p), + }) + } + + if allExist(dirs) { + return found, nil + } + + srcRoot := mod.Dir + if needClone { + clone, err := cloneModule(mod) + if err != nil { + return nil, err + } + // The clone is only a staging area; the vendored copy is what survives. + defer os.RemoveAll(clone) + srcRoot = clone + } + + for i, rel := range rels { + src := filepath.Join(srcRoot, filepath.FromSlash(rel)) + if err := copyProtos(src, dirs[i]); err != nil { + return nil, fmt.Errorf("vendoring %q: %w", want[i], err) + } + } + + return found, nil +} + +// anchor returns the directory a path is resolved against once its leading +// ".." components have been applied, which is the directory protoc has to be +// given as -I for that path's imports to resolve. +// +// Import paths are written relative to the repository's proto root, not to the +// Go module: "../values" under module directory "cre/go" holds +// "values/v1/values.proto", so the include directory is "cre", not "cre/values". +func anchor(base, p string) string { + dir := path.Clean(base) + for part := range strings.SplitSeq(path.Clean(p), "/") { + if part != ".." { + break + } + dir = path.Dir(dir) + } + return filepath.FromSlash(dir) +} + +// dedupe drops repeated entries, keeping the first of each. Sibling paths +// commonly share an anchor, and protoc has no use for the same -I twice. +func dedupe(values []string) []string { + seen := make(map[string]bool, len(values)) + out := values[:0] + for _, v := range values { + if !seen[v] { + seen[v] = true + out = append(out, v) + } + } + return out +} + +// module is the subset of `go list -m -json` output we need. +type module struct { + Path string + Version string + Dir string + Replace *module +} + +// localReplace reports whether the module is replaced by a directory on disk, +// which Go signals by a replacement with a path but no version. +func (m *module) localReplace() bool { + return m.Replace != nil && m.Replace.Version == "" +} + +// resolve asks the go tool where a module lives, following any replacement. An +// empty repo resolves the main module - the one being generated. +func resolve(dir, repo string) (*module, error) { + args := []string{"list", "-m", "-json"} + if repo != "" { + args = append(args, "--", repo) + } + + cmd := exec.Command("go", args...) + cmd.Dir = dir + cmd.Stderr = os.Stderr + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("%q is not in the build list, run `go mod tidy`: %w", repo, err) + } + + mod := &module{} + if err = json.Unmarshal(out, mod); err != nil { + return nil, err + } + + // Report the replacement's identity, but keep the replacement's own Dir: + // that is where the source actually is. + if mod.Replace != nil { + replace := *mod.Replace + if replace.Dir == "" { + replace.Dir = mod.Dir + } + replace.Replace = mod.Replace + mod = &replace + } + + if mod.Dir == "" && !mod.localReplace() { + return nil, fmt.Errorf("%q is not downloaded, run `go mod tidy`", repo) + } + + return mod, nil +} + +// escapesRoot reports whether any path points above the module root. +func escapesRoot(paths []string) bool { + for _, p := range paths { + if clean := path.Clean(p); clean == ".." || strings.HasPrefix(clean, "../") { + return true + } + } + return false +} + +// existsUnder reports whether every path is present under root. Only meaningful +// once the paths are known not to escape root. +func existsUnder(root string, paths []string) bool { + if root == "" { + return false + } + + full := make([]string, 0, len(paths)) + for _, p := range paths { + full = append(full, filepath.Join(root, filepath.FromSlash(p))) + } + return allExist(full) +} + +func allExist(paths []string) bool { + for _, p := range paths { + if _, err := os.Stat(p); err != nil { + return false + } + } + return true +} + +// splitRepo splits a module path into the repository that hosts it and the +// directory the module occupies inside that repository. +// +// Only the well-known hosts are handled: full go-get discovery would need a +// network round trip to a page we cannot clone from anyway. +func splitRepo(modPath string) (root, subdir string, err error) { + parts := strings.Split(modPath, "/") + switch { + case len(parts) < 3: + return "", "", fmt.Errorf("cannot determine the repository for module %q", modPath) + case parts[0] != "github.com" && parts[0] != "gitlab.com" && parts[0] != "bitbucket.org": + return "", "", fmt.Errorf("cannot determine the repository for module %q, only github.com, gitlab.com and bitbucket.org are supported", modPath) + } + + // A /vN major-version suffix is part of the module path, not the repository + // layout, so it is never a real directory. + if last := parts[len(parts)-1]; len(parts) > 3 && isMajorSuffix(last) { + parts = parts[:len(parts)-1] + } + + return strings.Join(parts[:3], "/"), strings.Join(parts[3:], "/"), nil +} + +func isMajorSuffix(s string) bool { + if !strings.HasPrefix(s, "v") || len(s) < 2 { + return false + } + for _, r := range s[1:] { + if r < '0' || r > '9' { + return false + } + } + return true +} + +// cloneModule shallow-clones the repository the module lives in at the exact +// revision the build list pins, and returns the temporary checkout directory. +// The caller owns it. +func cloneModule(mod *module) (string, error) { + root, subdir, err := splitRepo(mod.Path) + if err != nil { + return "", err + } + + dir, err := os.MkdirTemp("", "proto-vendor-") + if err != nil { + return "", err + } + + ref, named := gitRef(mod.Version, subdir) + + // A tag can be fetched on its own, so ask for nothing else. + fetch := []string{"-C", dir, "fetch", "--quiet", "--no-tags", "--depth", "1", "origin", ref} + checkout := "FETCH_HEAD" + if !named { + // A pseudo-version only carries an abbreviated commit, which a remote + // refuses to resolve in a fetch request. Take every commit instead, but + // no trees or blobs, and let the checkout fault in just the revision we + // asked for. + fetch = []string{"-C", dir, "fetch", "--quiet", "--filter=tree:0", "--no-tags", "origin"} + checkout = ref + } + + // git init plus a targeted fetch, rather than `git clone`, to avoid paying + // for history and branches nothing here reads. + steps := [][]string{ + {"init", "--quiet", dir}, + {"-C", dir, "remote", "add", "origin", "https://" + root}, + fetch, + {"-C", dir, "checkout", "--quiet", checkout}, + } + for _, args := range steps { + cmd := exec.Command("git", args...) + cmd.Stderr = os.Stderr + if err = cmd.Run(); err != nil { + os.RemoveAll(dir) + return "", fmt.Errorf("git %s: %w", strings.Join(args, " "), err) + } + } + + return dir, nil +} + +// gitRef maps a module version to the revision that holds it, and reports +// whether that revision is a name the remote can look up. A pseudo-version +// yields its (abbreviated) commit; anything else is a tag, prefixed with the +// module's subdirectory when the module is not at the repository root. +func gitRef(version, subdir string) (ref string, named bool) { + if m := pseudoVersion.FindStringSubmatch(version); m != nil { + return m[1], false + } + + version = strings.TrimSuffix(version, "+incompatible") + if subdir == "" { + return version, true + } + return subdir + "/" + version, true +} + +// copyProtos copies the .proto files at src into dst, mirroring the directory +// structure. Everything else is left behind: only the schemas are needed, and +// the rest of a repository can be large. +func copyProtos(src, dst string) error { + info, err := os.Stat(src) + if err != nil { + return err + } + + if !info.IsDir() { + return copyFile(src, dst) + } + + return filepath.WalkDir(src, func(p string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + rel, err := filepath.Rel(src, p) + if err != nil { + return err + } + + if entry.IsDir() { + // .git in particular, but any dot directory is tooling, not schema. + if rel != "." && strings.HasPrefix(entry.Name(), ".") { + return fs.SkipDir + } + return nil + } + + if !entry.Type().IsRegular() || filepath.Ext(p) != ".proto" { + return nil + } + + return copyFile(p, filepath.Join(dst, rel)) + }) +} + +func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), dirPerm); err != nil { + return err + } + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + if _, err = io.Copy(out, in); err != nil { + return err + } + + return out.Close() +} + +// escapePath applies the module cache's case encoding, so that module paths +// differing only in case cannot collide on a case-insensitive file system. +func escapePath(modPath string) string { + escaped := strings.Builder{} + for _, r := range modPath { + if r >= 'A' && r <= 'Z' { + escaped.WriteByte('!') + r += 'a' - 'A' + } + escaped.WriteRune(r) + } + return escaped.String() +} diff --git a/libs/standalone/protohelpers/generator/generator.go b/libs/standalone/protohelpers/generator/generator.go new file mode 100644 index 000000000..f1eca409d --- /dev/null +++ b/libs/standalone/protohelpers/generator/generator.go @@ -0,0 +1,238 @@ +// Package generator generates the Go code for a capability's protos. It is run +// from the root of the capability's module, and generates every .proto in that +// module's protos directory into the Go package of the same name. +// +// It replaces the generator in chainlink-protos' installer, which compiles +// against a copy of the CRE protos embedded in that module: the copy is only as +// current as the release it was embedded in, and it cannot see a proto that +// lives anywhere else. This one takes the .proto sources of whatever version of +// chainlink-protos the capability already depends on, so the schemas and the +// generated Go code cannot drift apart, and it generates a capability's own +// .proto from where the capability keeps it - beside the capability, in this +// repository - rather than requiring it to be checked in to chainlink-protos +// first. +package generator + +import ( + "fmt" + "io/fs" + "maps" + "os" + "path" + "path/filepath" +) + +// Protos is where a capability keeps its .proto files, and therefore also the +// Go package they generate into. It is a convention rather than a setting: a +// capability that imports another finds its protos by it, without needing to +// know anything about that capability beyond its module path. +const Protos = "protos" + +// creModule holds the protos every capability is written against: the values +// its inputs and outputs are made of, the SDK types a capability's methods take +// and return, and the annotations that mark a service as a capability. +const creModule = "github.com/smartcontractkit/chainlink-protos/cre/go" + +// creSources are the directories of creModule's repository to vendor. They sit +// above the Go module's root, which is why they never reach the module cache +// and have to be vendored at all. +var creSources = []string{"../values", "../sdk", "../tools"} + +// creLinks is the Go package each vendored CRE proto is already generated into. +// Generating them again here would give a capability its own copies of types the +// CRE runtime hands it, which would not be the same Go types. +var creLinks = map[string]string{ + "values/v1/values.proto": creModule + "/values/pb", + "sdk/v1alpha/sdk.proto": creModule + "/sdk", + "tools/generator/v1alpha/cre_metadata.proto": creModule + "/tools/generator", +} + +// Generator generates a capability's protos with a chosen set of protoc +// plugins. +type Generator struct { + // Plugins are the protoc code generators to run over every .proto, each one + // building on the same includes and the same package links. + Plugins []Plugin +} + +// Generate runs what a capability needs: the messages, and the server for each +// service. A capability wanting more than that - a client, a mock - builds a +// Generator with those plugins instead. +func Generate(capabilities ...string) error { + return (&Generator{Plugins: []Plugin{GoPlugin, CREPlugin}}).Generate(capabilities...) +} + +// Generate generates the protos of the module the working directory is in. +// Everything is resolved from that module's root, so it does not matter where +// under the module it is run from - a go:generate directive can sit beside the +// main that calls this rather than at the module root. +// +// capabilities are the module paths of other capabilities whose protos these +// ones import. Each is resolved against this module's go.mod, its protos are +// compiled alongside, and they are linked to the Go package that capability +// already generated them into - which is its module path plus protos, by the +// same convention this generator writes. +func (g *Generator) Generate(capabilities ...string) error { + if len(g.Plugins) == 0 { + return fmt.Errorf("no protoc plugins configured") + } + + mod, err := resolve(".", "") + if err != nil { + return err + } + + dir := filepath.Join(mod.Dir, Protos) + + files, err := filepath.Glob(filepath.Join(dir, "*.proto")) + if err != nil { + return err + } + if len(files) == 0 { + return fmt.Errorf("no .proto files in %s", dir) + } + + // Everything protoc compiles is read from one staging directory, so that a + // .proto is compiled under the path its own proto package declares, wherever + // its source is kept. See stage. + staging, err := os.MkdirTemp("", "protos-") + if err != nil { + return err + } + defer os.RemoveAll(staging) + + run := &protoc{Plugins: g.Plugins, Includes: []string{staging}, Links: map[string]string{}} + + staged, err := stage(staging, files) + if err != nil { + return err + } + for _, file := range staged { + run.Links[file] = path.Join(mod.Path, Protos) + } + + items := map[string][]string{creModule: creSources} + for _, capability := range capabilities { + items[capability] = []string{Protos} + } + + vendored, err := (&downloader{Items: items, Dir: mod.Dir, Vendor: repoRoot(mod.Dir)}).download() + if err != nil { + return err + } + + for _, v := range vendored { + // The CRE protos are laid out by their proto package already, so they + // are compiled where they were vendored rather than staged. + if v.Module == creModule { + run.Includes = append(run.Includes, v.Include) + continue + } + + if err = link(run, staging, v.Dir, path.Join(v.Module, Protos)); err != nil { + return fmt.Errorf("%s: %w", v.Module, err) + } + } + + run.Includes = dedupe(run.Includes) + maps.Copy(run.Links, creLinks) + + // Every plugin binary is built from the version this module depends on, so + // that regenerating cannot pick up whatever happens to be on PATH. + tools, err := os.MkdirTemp("", "protoc-plugins-") + if err != nil { + return err + } + defer os.RemoveAll(tools) + + run.Tools = tools + for _, plugin := range run.Plugins { + if err = plugin.install(tools, mod.Dir); err != nil { + return fmt.Errorf("installing protoc-gen-%s: %w", plugin.Name, err) + } + } + + // Generated files land under their .proto's path, which is a proto + // namespace rather than anything belonging in a Go module, so they are + // generated aside and then moved into the protos package. + out, err := os.MkdirTemp("", "protos-go-") + if err != nil { + return err + } + defer os.RemoveAll(out) + + if err = run.generate(out, staged); err != nil { + return err + } + + return move(out, dir) +} + +// repoRoot returns the root of the repository dir is in, or dir itself if it is +// not in one. +// +// The vendored .proto sources are keyed by module and version, so every module +// of every capability in a repository can be served out of one cache rather +// than fetching the same chainlink-protos release once per module. +func repoRoot(dir string) string { + for at := dir; ; { + // A worktree's .git is a file rather than a directory, so this only + // asks whether it exists. + if _, err := os.Stat(filepath.Join(at, ".git")); err == nil { + return at + } + + parent := filepath.Dir(at) + if parent == at { + return dir + } + at = parent + } +} + +// link stages the protos of another capability this one imports, so that they +// can be imported by the path their proto package declares, and links them to +// the Go package that capability generated them into. +func link(run *protoc, staging, dir, goPkg string) error { + files, err := filepath.Glob(filepath.Join(dir, "*.proto")) + if err != nil { + return err + } + if len(files) == 0 { + return fmt.Errorf("no .proto files in %s", dir) + } + + staged, err := stage(staging, files) + if err != nil { + return err + } + + for _, file := range staged { + run.Links[file] = goPkg + } + + return nil +} + +// move flattens the generated tree into the protos package: a Go package is one +// directory, whatever nesting the proto namespace has. +// +// Everything generated is moved, rather than the files a plugin is expected to +// have written, since each plugin names its output as it likes. A plugin that +// generates into subdirectories of its own - a package of mocks, say - will +// need more than a flattening here. +func move(from, to string) error { + return filepath.WalkDir(from, func(p string, entry fs.DirEntry, err error) error { + if err != nil || entry.IsDir() { + return err + } + + dst := filepath.Join(to, entry.Name()) + if err = os.Rename(p, dst); err != nil { + return err + } + + fmt.Println("Generated", dst) + return nil + }) +} diff --git a/libs/standalone/protohelpers/generator/plugin.go b/libs/standalone/protohelpers/generator/plugin.go new file mode 100644 index 000000000..5efca973e --- /dev/null +++ b/libs/standalone/protohelpers/generator/plugin.go @@ -0,0 +1,80 @@ +package generator + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// Plugin is a protoc code generator: protoc runs it as protoc-gen-, and +// drives it with --_out and --_opt. +type Plugin struct { + Name string + + // Package is the plugin's main package. The module holding it, and so the + // version of the plugin that runs, is whatever the capability being + // generated resolves that package to: the plugin is built from the commit + // the capability's go.mod pins, the same one its generator came from. + // + // That is what lets this generator change without breaking capabilities + // that have not moved yet. A capability pinning an older version keeps + // generating with the plugin from that version, so a plugin gaining an + // option, or emitting a new file, reaches a capability only when it updates + // - and updating the plugin and the library it generates against is one + // change, not two that can disagree. + Package string +} + +// GoPlugin generates the Go structs, via protoc-gen-go. +var GoPlugin = Plugin{ + Name: "go", + Package: "google.golang.org/protobuf/cmd/protoc-gen-go", +} + +// CREPlugin generates the server for each capability service in a proto. +var CREPlugin = Plugin{ + Name: "cre", + Package: "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/protoc", +} + +// install builds the plugin's binary into dir, from the source the module in +// from resolves its package to. +func (p Plugin) install(dir, from string) error { + list := exec.Command("go", "list", "-f", "{{.Dir}}", "--", p.Package) + list.Dir = from + list.Stderr = os.Stderr + + out, err := list.Output() + if err != nil { + return fmt.Errorf("%q is not in the build list, run `go get %s`: %w", p.Package, p.Package, err) + } + + pkg := strings.TrimSpace(string(out)) + if pkg == "" { + return fmt.Errorf("%q is not downloaded, run `go mod download`", p.Package) + } + + tools, err := filepath.Abs(dir) + if err != nil { + return err + } + + // Named rather than left to go build, which would name the binary after the + // package's directory - right for cmd/protoc-gen-go, wrong for a plugin + // whose directory is not already called protoc-gen-. + build := exec.Command("go", "build", "-o", p.binary(tools), ".") + build.Dir = pkg + + if output, buildErr := build.CombinedOutput(); buildErr != nil { + return fmt.Errorf("%s in %s: %w\n%s", build.String(), pkg, buildErr, output) + } + + return nil +} + +// binary is the path install built the plugin to. +func (p Plugin) binary(dir string) string { + return filepath.Join(dir, "protoc-gen-"+p.Name) +} diff --git a/libs/standalone/protohelpers/generator/protoc.go b/libs/standalone/protohelpers/generator/protoc.go new file mode 100644 index 000000000..550def126 --- /dev/null +++ b/libs/standalone/protohelpers/generator/protoc.go @@ -0,0 +1,69 @@ +package generator + +import ( + "fmt" + "os/exec" + "sort" + "strings" +) + +// protoc is a configured protoc invocation. +type protoc struct { + // Plugins are the code generators to run, each over every file, with the + // same includes and the same links. + Plugins []Plugin + + // Includes are the directories protoc resolves imports against, in order. + Includes []string + + // Links are the Go import path each .proto generates into, keyed by the + // .proto's path as written in an import statement. A .proto carrying no + // `option go_package` - the convention in the CRE protos - has to be linked + // before it can be generated or imported. + Links map[string]string + + // Tools is the directory the plugin binaries were installed to. + Tools string +} + +// generate writes the code for files, which are paths relative to one of the +// include directories, under out. Generation is source-relative: a package's Go +// import path is decided by Links, not by where protoc puts the file. +func (p *protoc) generate(out string, files []string) error { + var args []string + for _, include := range p.Includes { + args = append(args, "-I", include) + } + + // Sorted so that a run's arguments, and so its output, do not depend on map + // iteration order. + linked := make([]string, 0, len(p.Links)) + for file := range p.Links { + linked = append(linked, file) + } + sort.Strings(linked) + + for _, plugin := range p.Plugins { + args = append(args, + fmt.Sprintf("--plugin=protoc-gen-%s=%s", plugin.Name, plugin.binary(p.Tools)), + fmt.Sprintf("--%s_out=%s", plugin.Name, out), + fmt.Sprintf("--%s_opt=paths=source_relative", plugin.Name), + ) + + for _, file := range linked { + args = append(args, fmt.Sprintf("--%s_opt=M%s=%s", plugin.Name, file, p.Links[file])) + } + } + + cmd := exec.Command("protoc", append(args, files...)...) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w\n%s", cmd.String(), err, output) + } + + if trimmed := strings.TrimSpace(string(output)); trimmed != "" { + fmt.Println(trimmed) + } + + return nil +} diff --git a/libs/standalone/protohelpers/generator/stage.go b/libs/standalone/protohelpers/generator/stage.go new file mode 100644 index 000000000..12c7f0d8a --- /dev/null +++ b/libs/standalone/protohelpers/generator/stage.go @@ -0,0 +1,54 @@ +package generator + +import ( + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strings" +) + +// protoPackage captures the name in a .proto's package declaration. Anything +// after a "//" is dropped first, so a commented-out declaration cannot match. +var protoPackage = regexp.MustCompile(`(?m)^[^/\n]*\bpackage\s+([A-Za-z0-9_.]+)\s*;`) + +// stage copies each file into dir at the path its proto package declares, and +// returns those paths, in the order the files were given. +// +// The path a .proto is compiled under is baked into the generated descriptor, +// and is what other protos import it by, so it has to follow the proto package +// - package capabilities.internal.consensus.v1alpha compiles as +// capabilities/internal/consensus/v1alpha/consensus.proto - however the file is +// laid out on disk. Keeping a capability's .proto next to the capability is a +// layout choice; the namespace it generates into is not, and staging a copy is +// what keeps the two independent. +func stage(dir string, files []string) ([]string, error) { + staged := make([]string, 0, len(files)) + for _, file := range files { + content, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + match := protoPackage.FindSubmatch(content) + if match == nil { + return nil, fmt.Errorf("%s declares no proto package", file) + } + + name := path.Join(strings.ReplaceAll(string(match[1]), ".", "/"), filepath.Base(file)) + dst := filepath.Join(dir, filepath.FromSlash(name)) + + if err = os.MkdirAll(filepath.Dir(dst), 0700); err != nil { + return nil, err + } + + if err = os.WriteFile(dst, content, 0600); err != nil { + return nil, err + } + + staged = append(staged, name) + } + + return staged, nil +} diff --git a/libs/standalone/protohelpers/protoc/main.go b/libs/standalone/protohelpers/protoc/main.go new file mode 100644 index 000000000..cb58ad99d --- /dev/null +++ b/libs/standalone/protohelpers/protoc/main.go @@ -0,0 +1,63 @@ +// Command protoc-gen-cre generates the server that serves a capability's proto +// service: the typed interface a capability implements, and the untyped +// capabilities.ExecutableAndTriggerCapability a host registers and serves. +// +// It is chainlink-common's plugin without the parts a host does: nothing here +// initialises a capability, adds it to a registry or takes it back out, because +// the standalone bootstrapper (libs/standalone/capability) is what hosts a +// capability and already does all three. What is left is the translation between +// the untyped capability API and the typed methods generated from the proto, +// which is the only part that has to be generated per service. +// +// The generated file goes in the same package as the messages it is generated +// alongside - a capability's protos directory - so a capability is one package +// rather than a package and a server subpackage of it. +package main + +import ( + _ "embed" + "log" + "os" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/types/pluginpb" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/protoc/pkg" +) + +const ( + toolName = "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/protoc" + localPrefix = "github.com/smartcontractkit/capabilities" +) + +//go:embed server.go.tmpl +var serverTemplate string + +func main() { + protogen.Options{}.Run(func(plugin *protogen.Plugin) error { + plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + + for _, file := range plugin.Files { + // A file with no service carries messages only, and has no server + // to generate - the Go plugin has already generated all of it. + if !file.Generate || len(file.Services) == 0 { + continue + } + + template := &pkg.TemplateGenerator{ + Name: "capability_server", + Template: serverTemplate, + FileNameTemplate: "{{.}}_server_gen.go", + StringLblValue: pkg.StringLblValue(true), + PbLabelTLangLabels: pkg.PbLabelToGoLabels, + } + + if err := template.GenerateFile(file, plugin, file, toolName, localPrefix); err != nil { + log.Printf("failed to generate for %s: %v", file.Desc.Path(), err) + os.Exit(1) + } + } + + return nil + }) +} diff --git a/libs/standalone/protohelpers/protoc/server.go.tmpl b/libs/standalone/protohelpers/protoc/server.go.tmpl new file mode 100644 index 000000000..ec931f825 --- /dev/null +++ b/libs/standalone/protohelpers/protoc/server.go.tmpl @@ -0,0 +1,232 @@ +package {{.GoPackageName}} + +import ( + "fmt" + "context" + + "google.golang.org/protobuf/types/known/emptypb" + + {{- $self := printf "%s" .GoImportPath }} + {{- range .Services }} + {{- range .Methods }} + {{- addImport .Input.GoIdent.GoImportPath $self }} + {{- addImport .Output.GoIdent.GoImportPath $self }} + {{- end }} + {{- end }} + + {{- range allimports }} + {{.}} + {{- end }} + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" +) + +// Avoid unused imports if there is configuration type +var _ = emptypb.Empty{} + +{{- range .Services}} +{{ $hasTriggers := false }} +{{ $hasActions := false }} +{{ $service := . }} +{{ $fullCapabilityId := FullCapabilityId .}} +// {{.GoName}}Capability is what a capability implements to be served as {{CapabilityId $service}}. +// +// It carries no Initialise: a capability is given what it needs when it is +// built, and everything a host does to it - registering it, serving it, +// announcing it, taking it back out - belongs to the bootstrapper that hosts it +// rather than to the capability or to this server. +type {{.GoName}}Capability interface { + {{- range .Methods}} + {{- if isTrigger . }} + {{ $hasTriggers = true }} + Register{{.GoName}}(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *{{name .Input.GoIdent $self}}) (<- chan capabilities.TriggerAndId[*{{name .Output.GoIdent $self}}], caperrors.Error) + Unregister{{.GoName}}(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *{{name .Input.GoIdent $self}}) caperrors.Error + {{- else }} + {{ $hasActions = true }} + {{.GoName}}(ctx context.Context, metadata capabilities.RequestMetadata, input *{{name .Input.GoIdent $self}} {{if ne "emptypb.Empty" (ConfigType $service)}}, {{(ConfigType $service)}}{{ end }}) (*capabilities.ResponseAndMetadata[*{{name .Output.GoIdent $self}}], caperrors.Error) + {{- end }} + {{- end }} + + {{- if $hasTriggers }} + AckEvent(ctx context.Context, triggerId string, eventId string, method string) caperrors.Error + {{- end }} + + {{ range Labels . }} + {{.Name}}() {{.Type}} + {{ end }} + + Start(ctx context.Context) error + Close() error + HealthReport() map[string]error + Name() string + Description() string + Ready() error +} + +func New{{.GoName}}Server(capability {{.GoName}}Capability) *{{.GoName}}Server { + stopCh := make(chan struct{}) + return &{{.GoName}}Server{ + {{.GoName|LowerFirst}}Capability: {{.GoName|LowerFirst}}Capability{ {{.GoName}}Capability: capability, stopCh: stopCh}, + stopCh: stopCh, + } +} + +// {{.GoName}}Server serves the capability: it turns the untyped requests a host +// delivers into calls on the typed methods above, and is itself the +// capabilities.ExecutableAndTriggerCapability a host registers and serves. +type {{.GoName}}Server struct { + {{.GoName|LowerFirst}}Capability + stopCh chan struct{} +} + +// Close stops answering registered triggers, then closes the capability. +// +// Nothing is deregistered here. What put this capability in a registry, and +// announced it to a node, is the host - so taking it back out is the host's too, +// and doing it from both would race a shutdown against itself. +func (c *{{.GoName}}Server) Close() error{ + if c.stopCh != nil { + close(c.stopCh) + } + + return c.{{.GoName|LowerFirst}}Capability.Close() +} + +type {{.GoName|LowerFirst}}Capability struct { + {{.GoName}}Capability + stopCh chan struct{} +} + +func (c *{{.GoName|LowerFirst}}Capability) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { + return capabilities.NewCapabilityInfo({{$fullCapabilityId}}, capabilities.CapabilityTypeCombined, c.{{.GoName}}Capability.Description()) +} + + +var _ capabilities.ExecutableAndTriggerCapability = (*{{.GoName|LowerFirst}}Capability)(nil) + +const {{.GoName}}ID = "{{CapabilityId $service}}" + +func (c *{{.GoName|LowerFirst}}Capability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { +{{- if $hasTriggers }} + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + {{- range .Methods}} + {{- if (isTrigger .) }} + case {{- if (MapToUntypedAPI .) }} "" {{- else}} "{{.GoName}}" {{- end }}: + input := &{{name .Input.GoIdent $self}}{} + return capabilities.RegisterTrigger(ctx, c.stopCh, {{$fullCapabilityId}}, request, input, c.{{$service.GoName}}Capability.Register{{.GoName}}) + {{- end }} + {{- end }} + default: + return nil, fmt.Errorf("trigger %s not found", request.Method) + } +{{- else }} + return nil, fmt.Errorf("trigger %s not found", request.Method) +{{- end }} +} + +func (c *{{.GoName|LowerFirst}}Capability) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error { +{{- if $hasTriggers }} + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + {{- range .Methods}} + {{- if (isTrigger .) }} + case {{- if (MapToUntypedAPI .) }} "" {{- else}} "{{.GoName}}" {{- end }}: + input := &{{name .Input.GoIdent $self}}{} + _, err := capabilities.FromValueOrAny(request.Config, request.Payload, input) + if err != nil { + return err + } + return c.{{$service.GoName}}Capability.Unregister{{.GoName}}(ctx, request.TriggerID, request.Metadata, input) + {{- end }} + {{- end }} + default: + return fmt.Errorf("method %s not found", request.Method) + } +{{- else }} + return fmt.Errorf("trigger %s not found", request.Method) +{{- end }} +} + +func (c *{{.GoName|LowerFirst}}Capability) AckEvent(ctx context.Context, triggerId string, eventId string, method string) error { +{{- if $hasTriggers }} + switch method { + {{- range .Methods}} + {{- if (isTrigger .) }} + case {{- if (MapToUntypedAPI .) }} "" {{- else}} "{{.GoName}}" {{- end }}: + return c.{{$service.GoName}}Capability.AckEvent(ctx, triggerId, eventId, method) + {{- end }} + {{- end }} + default: + return fmt.Errorf("trigger %s not found", method) + } +{{- else }} + return fmt.Errorf("trigger %s not found", method) +{{- end }} +} + +func (c *{{.GoName|LowerFirst}}Capability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (c *{{.GoName|LowerFirst}}Capability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} + +func (c *{{.GoName|LowerFirst}}Capability) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { +{{- if $hasActions }} + response := capabilities.CapabilityResponse{} + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + {{- range .Methods}} + {{- if not (isTrigger .) }} + case "{{.GoName}}": + input := &{{name .Input.GoIdent $self}}{} + config := &{{ConfigType $service}}{} + {{- if eq "emptypb.Empty" (ConfigType $service) }} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *{{name .Input.GoIdent $self}}, _ *emptypb.Empty) (*{{name .Output.GoIdent $self}}, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.{{$service.GoName}}Capability.{{.GoName}}(ctx, metadata, input) + if err != nil { + return nil, capabilities.ResponseMetadata{}, nil, err + } + if output == nil { + return nil, capabilities.ResponseMetadata{}, nil, fmt.Errorf("output and error is nil for method {{.GoName}}(..) (if output is nil error must be present)") + } + return output.Response, output.ResponseMetadata, output.OCRAttestation, err + } + return capabilities.Execute(ctx, request, input, config, wrapped) + {{- else }} + return capabilities.Execute(ctx, request, input, config, c.{{.GoName}}Capability.{{.GoName}}) + {{- end }} + {{- if (MapToUntypedAPI .) }} + case "": + input := &{{name .Input.GoIdent $self}}{} + config := &{{ConfigType $service}}{} + {{- if eq "emptypb.Empty" (ConfigType $service) }} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *{{name .Input.GoIdent $self}}, _ *emptypb.Empty) (*{{name .Output.GoIdent $self}}, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.{{$service.GoName}}Capability.{{.GoName}}(ctx, metadata, input) + if err != nil { + return nil, capabilities.ResponseMetadata{}, nil, err + } + if output == nil { + return nil, capabilities.ResponseMetadata{}, nil, fmt.Errorf("output and error is nil for method {{.GoName}}(..) (if output is nil error must be present)") + } + return output.Response, output.ResponseMetadata, output.OCRAttestation, err + } + return capabilities.Execute(ctx, request, input, config, wrapped) + {{- else }} + return capabilities.Execute(ctx, request, input, config, c.{{.GoName}}Capability.{{.GoName}}) + {{- end }} + {{- end }} + {{- end }} + {{- end }} + default: + return response, fmt.Errorf("method %s not found", request.Method) + } +{{- else }} + return capabilities.CapabilityResponse{}, fmt.Errorf("method %s not found", request.Method) +{{- end }} +} + +{{- end }} diff --git a/libs/x/registry/ocr.go b/libs/x/registry/ocr.go new file mode 100644 index 000000000..f2ab23f83 --- /dev/null +++ b/libs/x/registry/ocr.go @@ -0,0 +1,82 @@ +package registry + +import ( + "context" + "fmt" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/capabilities/libs/ocr" + + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +var _ core.OCRConfigRegistry = (*Registry)(nil) + +// OCRConfig returns the OCR3 configuration a capability runs under, digest +// included. +// +// A capability hosted in another process is given this rather than the contract +// it came from: the digest is the only part of the contract's identity its +// oracle needs, and computing one here means every consumer of a configuration +// agrees on it by construction rather than by configuring the same chain and +// address twice. +func (r *Registry) OCRConfig(ctx context.Context, capabilityID string, donID uint32, key string) (ocrtypes.ContractConfig, error) { + cfg, digest, err := r.ocrConfig(ctx, capabilityID, donID, key) + if err != nil { + return ocrtypes.ContractConfig{}, err + } + return capabilitiespb.OCR3ConfigFromProto(cfg, digest) +} + +// ocrConfig resolves one OCR instance's configuration and its digest, in the +// form the contract stores it. The gRPC service passes both on as they are, so +// that a caller decoding the configuration and this computing its digest are +// looking at the same bytes. +func (r *Registry) ocrConfig(ctx context.Context, capabilityID string, donID uint32, key string) (*capabilitiespb.OCR3Config, ocrtypes.ConfigDigest, error) { + // A capability normally runs one OCR instance, and naming it is a detail + // only a capability running several has to care about. + if key == "" { + key = capabilitiespb.OCR3ConfigDefaultKey + } + + lr, err := r.snapshot() + if err != nil { + return nil, ocrtypes.ConfigDigest{}, err + } + + // Without a contract there is no digest to compute, and a digest computed + // from a zero chain and address would be one no other oracle agrees with - + // which fails as a silently unreachable DON rather than as an error. + if lr.Contract.Address == "" { + return nil, ocrtypes.ConfigDigest{}, fmt.Errorf( + "cannot compute the OCR config digest for capability %s: this registry does not report which contract it reads", capabilityID) + } + + raw, err := lr.RawConfigForCapability(ctx, capabilityID, donID) + if err != nil { + return nil, ocrtypes.ConfigDigest{}, err + } + + capConfig := &capabilitiespb.CapabilityConfig{} + if err = proto.Unmarshal(raw, capConfig); err != nil { + return nil, ocrtypes.ConfigDigest{}, fmt.Errorf( + "capability %s on DON %d has an unparseable config: %w", capabilityID, donID, err) + } + + cfg, ok := capConfig.GetOcr3Configs()[key] + if !ok { + return nil, ocrtypes.ConfigDigest{}, fmt.Errorf( + "capability %s on DON %d has no OCR config %q", capabilityID, donID, key) + } + + digest, err := ocr.ConfigDigest(lr.Contract.ChainID, lr.Contract.Address, capabilityID, donID, key, cfg) + if err != nil { + return nil, ocrtypes.ConfigDigest{}, fmt.Errorf( + "failed to compute the OCR config digest for capability %s on DON %d: %w", capabilityID, donID, err) + } + + return cfg, digest, nil +} diff --git a/libs/x/registry/server.go b/libs/x/registry/server.go index 14082fed0..08d35d00d 100644 --- a/libs/x/registry/server.go +++ b/libs/x/registry/server.go @@ -113,6 +113,18 @@ func (s *Server) NodeByPeerID(ctx context.Context, req *registrypb.NodeRequest) return nodeToProto(n), nil } +// OCRConfig serves the OCR3 configuration of one capability, with the digest +// this process computed for it. Both are passed on as the contract stores them, +// so that whoever decodes the configuration and whoever computed the digest +// were looking at the same bytes. +func (s *Server) OCRConfig(ctx context.Context, req *registrypb.OCRConfigRequest) (*registrypb.OCRConfigReply, error) { + cfg, digest, err := s.impl.ocrConfig(ctx, req.GetCapabilityId(), req.GetDonId(), req.GetKey()) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + return ®istrypb.OCRConfigReply{Config: cfg, ConfigDigest: digest[:]}, nil +} + func (s *Server) ConfigForCapability(ctx context.Context, req *registrypb.ConfigForCapabilityRequest) (*registrypb.ConfigForCapabilityReply, error) { raw, err := s.impl.RawConfigForCapability(ctx, req.GetCapabilityId(), req.GetDonId()) if err != nil { diff --git a/libs/x/registrysyncer/local_registry.go b/libs/x/registrysyncer/local_registry.go index 74aba764c..69b37f0e6 100644 --- a/libs/x/registrysyncer/local_registry.go +++ b/libs/x/registrysyncer/local_registry.go @@ -42,6 +42,7 @@ type ( NodeInfo = commonregistry.NodeInfo Capability = commonregistry.Capability Snapshot = commonregistry.Snapshot + Contract = commonregistry.Contract Reader = commonregistry.Reader ) @@ -61,6 +62,11 @@ type LocalRegistry struct { IDsToNodes map[ragetypes.PeerID]NodeInfo IDsToCapabilities map[string]Capability + // Contract is the registry these records were read from. Zero when whatever + // supplied them reads no contract, which is why anything needing it says so + // rather than assuming. + Contract Contract + cacheMu sync.RWMutex cachedLocalNodePeer ragetypes.PeerID cachedLocalNode capabilities.Node @@ -91,7 +97,11 @@ func FromSnapshot( getPeerID func() (ragetypes.PeerID, error), snap *Snapshot, ) *LocalRegistry { - return NewLocalRegistry(lggr, getPeerID, snap.DONs, snap.Nodes, snap.Capabilities) + lr := NewLocalRegistry(lggr, getPeerID, snap.DONs, snap.Nodes, snap.Capabilities) + // Set here rather than taken by NewLocalRegistry, whose signature is shared + // with core. + lr.Contract = snap.Contract + return lr } // LocalNode resolves this process's own node record. The result is cached: the From b453e86e836383b50e5bd6c7ca0a6f428caceef4 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Tue, 18 Aug 2026 15:08:46 -0400 Subject: [PATCH 21/30] Consensus cap --- consensus/action/capability.go | 29 ++++++----- consensus/go.mod | 4 ++ consensus/main.go | 21 ++++---- crecore/go.mod | 4 ++ crecore/signer_server.go | 5 +- libs/go.mod | 4 ++ libs/ocr/oracle.go | 23 ++++++++- libs/standalone/ocr/dependency.go | 8 ++++ libs/standalone/ocr/host.go | 8 +++- libs/standalone/ocr/proxy.go | 21 ++++++-- libs/standalone/ocr/proxy_test.go | 66 +++++++++++++++++++++++-- libs/standalone/ocr/signer.go | 77 +++++++++++++----------------- libs/standalone/ocr/signer_test.go | 46 ++++++++++++++++++ 13 files changed, 237 insertions(+), 79 deletions(-) create mode 100644 libs/standalone/ocr/signer_test.go diff --git a/consensus/action/capability.go b/consensus/action/capability.go index a1191c08c..3bd4050f1 100644 --- a/consensus/action/capability.go +++ b/consensus/action/capability.go @@ -116,6 +116,10 @@ type Dependencies struct { Offchain ocrtypes.OffchainKeyring Onchain ocr3types.OnchainKeyring[[]byte] + // TransmitAccount comes from there too: it is the node's name in the + // configuration, not something this capability transmits to. + TransmitAccount ocrtypes.Account + // Bootstrappers are the peers to dial before this oracle has heard of // anyone; the registry says who the oracle set is, not where it is. Bootstrappers []commontypes.BootstrapperLocator @@ -188,18 +192,19 @@ func NewConsensusCapability( } c.oracle, err = newOracle(libsocr.OracleArgs{ - CapabilityID: protos.ConsensusID, - DonID: deps.DonID, - Registry: deps.Registry, - Endpoints: deps.Endpoints, - Offchain: deps.Offchain, - Onchain: deps.Onchain, - Bootstrappers: deps.Bootstrappers, - Plugin: reportingPlugin, - Transmitter: transmitter.NewContractTransmitter(lggr, c.SendResponse), - LocalConfig: localOCRConfig, - Logger: lggr, - Metrics: deps.Metrics, + CapabilityID: protos.ConsensusID, + DonID: deps.DonID, + Registry: deps.Registry, + Endpoints: deps.Endpoints, + Offchain: deps.Offchain, + Onchain: deps.Onchain, + TransmitAccount: deps.TransmitAccount, + Bootstrappers: deps.Bootstrappers, + Plugin: reportingPlugin, + Transmitter: transmitter.NewContractTransmitter(lggr, c.SendResponse), + LocalConfig: localOCRConfig, + Logger: lggr, + Metrics: deps.Metrics, }) if err != nil { return nil, fmt.Errorf("error when creating oracle: %w", err) diff --git a/consensus/go.mod b/consensus/go.mod index 7b0d42994..be276989e 100644 --- a/consensus/go.mod +++ b/consensus/go.mod @@ -252,6 +252,10 @@ require ( replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common +// Matches the chainlink-common replace above: keystore is its own module, so a local +// chainlink-common is only half-applied without this. +replace github.com/smartcontractkit/chainlink-common/keystore => ../../chainlink-common/keystore + replace github.com/smartcontractkit/capabilities/libs => ../libs // Local override: cre/impl/proxy dropped peer-group proxying, only used for the capabilities diff --git a/consensus/main.go b/consensus/main.go index 158fdcd5e..57fc0aa27 100644 --- a/consensus/main.go +++ b/consensus/main.go @@ -81,14 +81,15 @@ run "docs" to write the full reference to docs/CONFIG.md.`, capabilityImpl, err := action.NewConsensusCapability(lggr, clockwork.NewRealClock(), responseCacheExpiry, cfg.ConsensusCapabilityConfig, action.Dependencies{ - DonID: deps.CapabilityDonID, - Registry: deps.OCRConfigRegistry(), - Endpoints: factories.OCR2Endpoint, - Offchain: factories.Offchain, - Onchain: factories.Onchain, - Bootstrappers: cfg.Bootstrappers.ToBootstrapperLocators(), - LimitsFactory: deps.LimitsFactory, - Metrics: scfg.MetricsRegisterer, + DonID: deps.CapabilityDonID, + Registry: deps.OCRConfigRegistry(), + Endpoints: factories.OCR2Endpoint, + Offchain: factories.Offchain, + Onchain: factories.Onchain, + TransmitAccount: factories.TransmitAccount, + Bootstrappers: cfg.Bootstrappers.ToBootstrapperLocators(), + LimitsFactory: deps.LimitsFactory, + Metrics: scfg.MetricsRegisterer, }) if err != nil { lggr.Fatalw("Failed to create ConsensusCapability", "error", err) @@ -108,10 +109,6 @@ run "docs" to write the full reference to docs/CONFIG.md.`, type Config struct { action.ConsensusCapabilityConfig `toml:",inline"` - // Bootstrappers are peerID@host:port entries to dial before this oracle has - // heard of anyone. The registry says who the DON's members are; it does not - // say where to find them, and an oracle that has met nobody cannot be told - // by the DON where the DON is. Bootstrappers config.BootstrapperLocators `usage:"peerID@host:port of the DON's bootstrap peers" example:"['12D3KooWFirst@127.0.0.1:6690']"` } diff --git a/crecore/go.mod b/crecore/go.mod index 2cc49f7ad..b23e6d183 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -294,6 +294,10 @@ require ( replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common +// Matches the chainlink-common replace above: keystore is its own module, so a local +// chainlink-common is only half-applied without this. +replace github.com/smartcontractkit/chainlink-common/keystore => ../../chainlink-common/keystore + replace github.com/smartcontractkit/chainlink-evm => ../../chainlink-evm replace github.com/smartcontractkit/capabilities/libs => ../libs diff --git a/crecore/signer_server.go b/crecore/signer_server.go index 2facb8c98..39b44ad6d 100644 --- a/crecore/signer_server.go +++ b/crecore/signer_server.go @@ -87,7 +87,10 @@ func (s *signerServer) SignReport(_ context.Context, req *creproxy.SignReportReq } copy(digest[:], req.GetConfigDigest()) - signature, err := s.bundle.Sign3(digest, req.GetSeqNr(), req.GetReport()) + // Signed through the shared helper rather than the bundle directly: the oracle + // asking for this signature verifies its peers' with the same code, so what a + // round's bytes are is decided in one place. + signature, err := ocr2key.SignOCR3Report(s.bundle, digest, req.GetSeqNr(), req.GetReport()) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } diff --git a/libs/go.mod b/libs/go.mod index 3fac54347..ce374b6e2 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -259,6 +259,10 @@ require ( replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common +// Matches the chainlink-common replace above: keystore is its own module, so a local +// chainlink-common is only half-applied without this. +replace github.com/smartcontractkit/chainlink-common/keystore => ../../chainlink-common/keystore + // Local override: cre/impl/proxy dropped peer-group proxying, only used for the capabilities // registry move that already has its own proto. replace github.com/smartcontractkit/chainlink-protos/cre/impl => ../../chainlink-protos/cre/impl diff --git a/libs/ocr/oracle.go b/libs/ocr/oracle.go index 75820bcae..f4bb9004f 100644 --- a/libs/ocr/oracle.go +++ b/libs/ocr/oracle.go @@ -13,6 +13,7 @@ import ( ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" "github.com/smartcontractkit/chainlink-common/pkg/types/core" ) @@ -46,6 +47,21 @@ type OracleArgs struct { Plugin ocr3types.ReportingPluginFactory[[]byte] Transmitter ocr3types.ContractTransmitter[[]byte] + // TransmitAccount is the account this oracle is registered to transmit from. + // It is part of the identity the configuration lists, alongside the peer ID + // and the public keys, and libocr checks all of them: an oracle whose account + // does not match its entry is not recognised as a member at all. + // + // It belongs here rather than to the capability because it is the node's, not + // the capability's - the same reason the keyrings and the peer are resolved + // from whoever holds the node's identity. A capability's own transmitter + // answers for where a report goes, which for a capability is usually back to + // whoever asked; who it is transmitting as is not its to know. + // + // Empty leaves the transmitter's own answer in place, which is what an + // embedded run wants: it joins no configuration written by anyone else. + TransmitAccount ocrtypes.Account + LocalConfig ocrtypes.LocalConfig Logger logger.Logger Metrics prometheus.Registerer @@ -69,12 +85,17 @@ func NewOracle(args OracleArgs) (libocr.Oracle, error) { metrics = prometheus.NewRegistry() } + transmitter := args.Transmitter + if args.TransmitAccount != "" { + transmitter = ocrcommon.TransmitterWithAccount(args.TransmitAccount, transmitter) + } + return libocr.NewOracle(libocr.OCR3OracleArgs2[[]byte]{ BinaryNetworkEndpointFactory: args.Endpoints, V2Bootstrappers: args.Bootstrappers, ContractConfigTracker: tracker, OffchainConfigDigester: digester{}, - ContractTransmitter: args.Transmitter, + ContractTransmitter: transmitter, ReportingPluginFactory: args.Plugin, Database: NewDatabase(args.CapabilityID, args.Logger), LocalConfig: args.LocalConfig, diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index 0f3aabeb5..ff2d8eadd 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -52,6 +52,14 @@ type OCRFactories struct { // registry metadata must know which node it is. PeerID ragetypes.PeerID + // TransmitAccount is the account the node is registered to transmit from, the third part of the + // identity an OCR configuration lists after the peer ID and the public keys. Resolved with them + // because it is the same identity seen from a third side, and an oracle that has two of the + // three is one libocr does not recognise. + // + // Empty for an embedded run, which joins no configuration written by anyone else. + TransmitAccount ocr2types.Account + // Keyrings sign this oracle's protocol messages and its reports. Resolved with the networking // because they come from the same place: whoever holds the node's identity holds both. Keyrings diff --git a/libs/standalone/ocr/host.go b/libs/standalone/ocr/host.go index ab2f9e978..5d6e8e6d1 100644 --- a/libs/standalone/ocr/host.go +++ b/libs/standalone/ocr/host.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/libocr/networking/rageping" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" "github.com/smartcontractkit/chainlink-common/pkg/config" commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" @@ -115,7 +116,12 @@ func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (* factories.OCR2 = bundle // Held here, so an oracle in this process signs with the same bundle this serves to oracles // elsewhere rather than going out over gRPC to reach a key it already has. - factories.Keyrings = Keyrings{Offchain: localKeyring{bundle}, Onchain: localKeyring{bundle}} + onchain, err := ocr2key.NewOCR3Keyring(evmFamily, bundle) + if err != nil { + return nil, err + } + // The bundle is already an offchain keyring, so only the onchain half is adapted. + factories.Keyrings = Keyrings{Offchain: bundle, Onchain: onchain} return factories, nil } diff --git a/libs/standalone/ocr/proxy.go b/libs/standalone/ocr/proxy.go index bee1f888b..70aa708a6 100644 --- a/libs/standalone/ocr/proxy.go +++ b/libs/standalone/ocr/proxy.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "google.golang.org/grpc" @@ -39,6 +40,12 @@ type ProxyConfig struct { // unmarshals text itself - so it is validated as a peer ID when the configuration is decoded // rather than wherever it is first used. PeerID ragetypes.PeerID `usage:"this node's rage p2p peer ID, hosted on its behalf by the proxy; required outside embed" example:"'12D3KooWKh28EhBVfiiFh39w3zqtBxzYJhmGfBZNmoL4tRjMWSor'"` + + // TransmitAccount is configured directly for the same reason the peer ID is: it is the node's + // name in the OCR configuration this process joins, it is a public value, and the process + // holding the keys it belongs to is the one that could look it up. An oracle whose account does + // not match its entry in the configuration is not a member of it. + TransmitAccount string `usage:"account this node is registered to transmit from; required outside embed" example:"'0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437'"` } // Proxy returns a standalone.BootstrapDependency that resolves the libocr Factories from proxy @@ -87,6 +94,9 @@ func (d *proxyDependency) Get(ctx context.Context, _ standalone.CommonConfig) (* if d.cfg.PeerID == (ragetypes.PeerID{}) { return nil, errors.New("--ocr.peer-id is required to delegate rage networking") } + if d.cfg.TransmitAccount == "" { + return nil, errors.New("--ocr.transmit-account is required to delegate rage networking") + } peerID := d.cfg.PeerID // The raw peer ID is passed to the endpoint factories, as libocr compares it against the peer @@ -121,10 +131,11 @@ func (d *proxyDependency) Get(ctx context.Context, _ standalone.CommonConfig) (* d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.cfg.ProxyAddress, "peerID", peerID.String()) return &OCRFactories{ - OCR2Endpoint: endpointFactory, - OCR3_1Endpoint: endpoint2Factory, - PeerID: peerID, - Keyrings: Keyrings{Offchain: keyring, Onchain: keyring}, - closer: multiCloser{endpointFactory, endpoint2Factory, conn}, + OCR2Endpoint: endpointFactory, + OCR3_1Endpoint: endpoint2Factory, + PeerID: peerID, + TransmitAccount: ocr2types.Account(d.cfg.TransmitAccount), + Keyrings: Keyrings{Offchain: keyring, Onchain: keyring}, + closer: multiCloser{endpointFactory, endpoint2Factory, conn}, }, nil } diff --git a/libs/standalone/ocr/proxy_test.go b/libs/standalone/ocr/proxy_test.go index f2a9786a6..a003c3dd5 100644 --- a/libs/standalone/ocr/proxy_test.go +++ b/libs/standalone/ocr/proxy_test.go @@ -1,11 +1,18 @@ package ocr import ( + "context" + "crypto/ed25519" + "net" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -17,9 +24,12 @@ func TestProxy(t *testing.T) { t.Run("the configured peer ID is the one reported and delegated for", func(t *testing.T) { peerID := mustPeerID(t, 7) + // Resolving this form reads the signer's public keys, so there has to be one to read: + // keyrings and networking are resolved together because they come from the same process. dep := &proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ - ProxyAddress: "127.0.0.1:50051", - PeerID: peerID, + ProxyAddress: serveSigner(t), + PeerID: peerID, + TransmitAccount: "0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437", }} factories, err := dep.Get(t.Context(), standalone.CommonConfig{}) @@ -31,6 +41,10 @@ func TestProxy(t *testing.T) { assert.Equal(t, peerID, factories.PeerID) assert.Equal(t, peerID.String(), factories.OCR2Endpoint.PeerID()) assert.Equal(t, peerID.String(), factories.OCR3_1Endpoint.PeerID()) + + // The third part of the identity the configuration lists, carried like the peer ID and for + // the same reason: it is public, and this process holds none of the keys behind it. + assert.Equal(t, ocr2types.Account("0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437"), factories.TransmitAccount) }) t.Run("a peer ID is decoded from its text form", func(t *testing.T) { @@ -43,13 +57,22 @@ func TestProxy(t *testing.T) { require.Error(t, decoded.UnmarshalText([]byte("not-a-peer-id"))) }) - t.Run("delegating needs both the proxy address and the peer ID", func(t *testing.T) { + t.Run("delegating needs the proxy address, the peer ID and the transmit account", func(t *testing.T) { _, err := (&proxyDependency{lggr: logger.Test(t)}).Get(t.Context(), standalone.CommonConfig{}) require.ErrorContains(t, err, "--ocr.proxy-address is required") _, err = (&proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ProxyAddress: "127.0.0.1:50051"}}). Get(t.Context(), standalone.CommonConfig{}) require.ErrorContains(t, err, "--ocr.peer-id is required") + + // Reported here rather than left to libocr: an oracle with no account is one the + // configuration does not list, which surfaces as a member no one recognises rather than as + // something missing. + _, err = (&proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ + ProxyAddress: "127.0.0.1:50051", + PeerID: mustPeerID(t, 7), + }}).Get(t.Context(), standalone.CommonConfig{}) + require.ErrorContains(t, err, "--ocr.transmit-account is required") }) t.Run("an embedded instance needs neither, deriving its identity instead", func(t *testing.T) { @@ -63,3 +86,40 @@ func TestProxy(t *testing.T) { assert.Equal(t, mustPeerID(t, 2), factories.PeerID) }) } + +// serveSigner starts a Signer serving fixed public keys and returns its address. +// +// Only the keys are needed: this covers what resolving the dependency reads, and a +// signature made by a fake key would prove nothing about the real one. See +// signer_test.go for the signing itself. +func serveSigner(t *testing.T) string { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + server := grpc.NewServer() + proxy.RegisterSignerServer(server, stubSigner{}) + go func() { _ = server.Serve(listener) }() + t.Cleanup(server.Stop) + + return listener.Addr().String() +} + +// stubSigner answers with well-formed public keys and refuses to sign: nothing here +// holds a key, and a caller reaching the signing calls would be testing this rather +// than the code under test. +type stubSigner struct { + proxy.UnimplementedSignerServer +} + +func (stubSigner) Keys(context.Context, *proxy.KeysRequest) (*proxy.KeysReply, error) { + return &proxy.KeysReply{ + // ed25519 sized, as the remote keyring checks. + OffchainPublicKey: make([]byte, ed25519.PublicKeySize), + ConfigEncryptionPublicKey: make([]byte, ed25519.PublicKeySize), + // An EVM onchain public key is an address. + OnchainPublicKey: make([]byte, 20), + MaxSignatureLength: 65, + }, nil +} diff --git a/libs/standalone/ocr/signer.go b/libs/standalone/ocr/signer.go index 8441ce469..a9199edfe 100644 --- a/libs/standalone/ocr/signer.go +++ b/libs/standalone/ocr/signer.go @@ -2,6 +2,7 @@ package ocr import ( "context" + "errors" "fmt" "golang.org/x/crypto/curve25519" @@ -58,9 +59,18 @@ func newRemoteKeyring(ctx context.Context, conn grpc.ClientConnInterface) (*remo return nil, fmt.Errorf("failed to read the signer's public keys: %w", err) } + // Encoded here rather than on each call: the signer serves the bundle's own + // public key, and what an oracle has to answer with is the form the config + // carries. Failing here reports a key that cannot be encoded where it is read, + // rather than as an oracle no one recognises. + onchainPublicKey, err := marshalEVMOnchainPublicKey(keys.GetOnchainPublicKey()) + if err != nil { + return nil, err + } + k := &remoteKeyring{ client: client, - onchainPublicKey: keys.GetOnchainPublicKey(), + onchainPublicKey: onchainPublicKey, maxSignatureLen: int(keys.GetMaxSignatureLength()), } @@ -145,6 +155,9 @@ func (k *remoteKeyring) MaxSignatureLength() int { return k.maxSignatureLen } // signed blob is the report bound to the round it belongs to, and the signature // is secp256k1 over it. // +// publicKey is a peer's key as the config carries it, so the EVM entry is taken +// out of it first - see multichain.go. +// // Only EVM, deliberately. A signature has to be verified against the scheme the // signer used, and guessing wrong would accept nothing rather than accept the // wrong thing - but it would do so silently, so the process holding the key @@ -157,51 +170,27 @@ func verifyReport( report ocrtypes.Report, signature []byte, ) bool { - return ocr2key.EvmVerifyBlob(publicKey, ocr2key.ReportToSigData3(digest, seqNr, report), signature) -} - -// localKeyring is a node key bundle used in the process that holds it. -// -// The bundle is an OCR2 keyring - it signs a report without knowing what a -// sequence number is - so this is the same adaptation the node makes when it -// runs an OCR3 capability itself: bind the report to its round, then sign that. -type localKeyring struct { - bundle ocr2key.KeyBundle -} - -var ( - _ ocrtypes.OffchainKeyring = localKeyring{} - _ ocr3types.OnchainKeyring[[]byte] = localKeyring{} -) - -func (k localKeyring) OffchainSign(msg []byte) ([]byte, error) { return k.bundle.OffchainSign(msg) } - -func (k localKeyring) ConfigDiffieHellman(point [curve25519.PointSize]byte) ([curve25519.PointSize]byte, error) { - return k.bundle.ConfigDiffieHellman(point) -} - -func (k localKeyring) OffchainPublicKey() ocrtypes.OffchainPublicKey { - return k.bundle.OffchainPublicKey() -} - -func (k localKeyring) ConfigEncryptionPublicKey() ocrtypes.ConfigEncryptionPublicKey { - return k.bundle.ConfigEncryptionPublicKey() + key, err := ocr2key.OnchainPublicKeyFor(evmFamily, publicKey) + if err != nil { + return false + } + return ocr2key.EvmVerifyBlob(key, ocr2key.ReportToSigData(ocr2key.OCR3ReportContext(digest, seqNr), report), signature) } -func (k localKeyring) PublicKey() ocrtypes.OnchainPublicKey { return k.bundle.PublicKey() } +// An OCR3 capability's members are registered with a multi-chain onchain public +// key: one length-prefixed entry per signing family rather than the bare key of +// whichever chain it happens to sign for. The codec is ocr2key's, shared with the +// node that writes those configs; what is here is the EVM-shaped view of it that a +// process delegating its signing needs. -func (k localKeyring) Sign(digest ocrtypes.ConfigDigest, seqNr uint64, report ocr3types.ReportWithInfo[[]byte]) ([]byte, error) { - return k.bundle.Sign3(digest, seqNr, report.Report) -} +// evmFamily is the name the EVM entry is keyed by, and the name core's jobs give +// the EVM bundle in onchainSigningStrategy.config. +const evmFamily = "evm" -func (k localKeyring) Verify( - publicKey ocrtypes.OnchainPublicKey, - digest ocrtypes.ConfigDigest, - seqNr uint64, - report ocr3types.ReportWithInfo[[]byte], - signature []byte, -) bool { - return k.bundle.Verify3(publicKey, digest, seqNr, report.Report, signature) +// marshalEVMOnchainPublicKey encodes a bare EVM key as the single-family form. +func marshalEVMOnchainPublicKey(key ocrtypes.OnchainPublicKey) (ocrtypes.OnchainPublicKey, error) { + if len(key) == 0 { + return nil, errors.New("no evm onchain public key to encode") + } + return ocr2key.MarshalMultichainPublicKey(map[string]ocrtypes.OnchainPublicKey{evmFamily: key}) } - -func (k localKeyring) MaxSignatureLength() int { return k.bundle.MaxSignatureLength() } diff --git a/libs/standalone/ocr/signer_test.go b/libs/standalone/ocr/signer_test.go new file mode 100644 index 000000000..b831dd511 --- /dev/null +++ b/libs/standalone/ocr/signer_test.go @@ -0,0 +1,46 @@ +package ocr + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +// The encoding is not this package's to choose - it is the one already in the +// configs these oracles join - so the EVM view of it is asserted as bytes rather +// than only round-tripped. A round trip would pass just as well against an +// encoding no config uses. The codec itself is ocr2key's, and tested there. +func TestMarshalEVMOnchainPublicKey(t *testing.T) { + t.Parallel() + + // A 20 byte EVM address, which is what an EVM bundle's PublicKey is. + key := ocrtypes.OnchainPublicKey{ + 0x1a, 0x0e, 0x37, 0x3a, 0x3b, 0xcb, 0x04, 0x96, 0xb0, 0x23, + 0x0a, 0xef, 0x64, 0x2f, 0x03, 0xcf, 0x52, 0x8b, 0x9f, 0x9c, + } + + encoded, err := marshalEVMOnchainPublicKey(key) + require.NoError(t, err) + + // family 1 (EVM), then the length as a little-endian uint16, then the key. + want := append(ocrtypes.OnchainPublicKey{0x01, 0x14, 0x00}, key...) + assert.Equal(t, want, encoded) + + got, err := ocr2key.OnchainPublicKeyFor(evmFamily, encoded) + require.NoError(t, err) + assert.Equal(t, key, got) +} + +func TestOnchainPublicKeysRejectsNoEVMEntry(t *testing.T) { + t.Parallel() + + // Aptos only: nothing here can check an EVM signature. + _, err := ocr2key.OnchainPublicKeyFor(evmFamily, ocrtypes.OnchainPublicKey{0x05, 0x01, 0x00, 0xaa}) + require.Error(t, err) + assert.Contains(t, err.Error(), "no evm entry") +} From 7ad81852ca1e1d1bc9b1e121257280ec43252265 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 19 Aug 2026 13:38:59 -0400 Subject: [PATCH 22/30] Standalone cron --- cron/go.mod | 117 +++++++++------ cron/go.sum | 237 ++++++++++++++++-------------- cron/main.go | 87 +++++++++-- cron/protos/gen/main.go | 25 ++++ cron/protos/trigger.pb.go | 228 ++++++++++++++++++++++++++++ cron/protos/trigger.proto | 33 +++++ cron/protos/trigger_server_gen.go | 139 ++++++++++++++++++ cron/trigger/trigger.go | 133 +++++++++-------- cron/trigger/trigger_test.go | 143 ++++++------------ 9 files changed, 815 insertions(+), 327 deletions(-) create mode 100644 cron/protos/gen/main.go create mode 100644 cron/protos/trigger.pb.go create mode 100644 cron/protos/trigger.proto create mode 100644 cron/protos/trigger_server_gen.go diff --git a/cron/go.mod b/cron/go.mod index d76d4aae6..19095e9d6 100644 --- a/cron/go.mod +++ b/cron/go.mod @@ -7,50 +7,54 @@ require ( github.com/google/uuid v1.6.0 github.com/jonboulle/clockwork v0.5.0 github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.uber.org/zap v1.27.1 + go.uber.org/zap v1.28.0 google.golang.org/protobuf v1.36.11 ) require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/XSAM/otelsql v0.37.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/XSAM/otelsql v0.42.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/buger/jsonparser v1.1.2 // indirect + github.com/buger/jsonparser v1.2.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect - github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dominikbraun/graph v0.23.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect - github.com/grafana/pyroscope-go v1.2.8 // indirect - github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grafana/pyroscope-go v1.3.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.8.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.14.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.9.2 // indirect @@ -58,19 +62,19 @@ require ( github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mr-tron/base58 v1.3.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/run v1.2.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect @@ -78,33 +82,40 @@ require ( github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/smartcontractkit/chain-selectors v1.0.100 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6 // indirect - github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b // indirect - github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 // indirect - github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect + github.com/smartcontractkit/chain-selectors v1.0.104 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect + github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect + github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect + github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 // indirect + github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect - github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // 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/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect @@ -113,6 +124,8 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect golang.org/x/net v0.55.0 // indirect @@ -120,9 +133,21 @@ require ( golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.82.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common + +// Matches the chainlink-common replace above: keystore is its own module, so a local +// chainlink-common is only half-applied without this. +replace github.com/smartcontractkit/chainlink-common/keystore => ../../chainlink-common/keystore + +replace github.com/smartcontractkit/capabilities/libs => ../libs + +// Local override: cre/impl/proxy dropped peer-group proxying, only used for the capabilities +// registry move that already has its own proto. +replace github.com/smartcontractkit/chainlink-protos/cre/impl => ../../chainlink-protos/cre/impl diff --git a/cron/go.sum b/cron/go.sum index f6f3aad2a..dd8502ea3 100644 --- a/cron/go.sum +++ b/cron/go.sum @@ -1,32 +1,34 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= -github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= -github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= -github.com/apache/arrow-go/v18 v18.3.1/go.mod h1:12QBya5JZT6PnBihi5NJTzbACrDGXYkrgjujz3MRQXU= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/XSAM/otelsql v0.42.0 h1:Li0xF4eJUxG2e0x3D4rvRlys1f27yJKvjTh7ljkUP5o= +github.com/XSAM/otelsql v0.42.0/go.mod h1:4mOrEv+cS1KmKzrvTktvJnstr5GtKSAK+QHvFR9OcpI= +github.com/apache/arrow-go/v18 v18.6.0 h1:GX/Jyd3R7mCLiECAwY9FWbbaYblie2WXBSz4Sw8fNpM= +github.com/apache/arrow-go/v18 v18.6.0/go.mod h1:gm3MiPpY82fLYK5VKPB3WoJbsiLVDfT7flD5/vHReKw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= -github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= +github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 h1:nLaJZcVAnaqch3K83AyzHfY2DmQM18/L7jvkmKSfkpI= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1/go.mod h1:6Q+F2puKpJ6zWv+R02BVnizJICf7++oRT5zwpZQAsbk= -github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= -github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCeN8rge3F/deT52w2WJMmIC5mHNUq+Ut8= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= +github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= +github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -38,16 +40,20 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-co-op/gocron/v2 v2.18.0 h1:DS3Uhru66q1jy/5f9V0itmi3cLXcn2b7N+duGfgT7gU= github.com/go-co-op/gocron/v2 v2.18.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= -github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -60,12 +66,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -84,8 +90,8 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -102,24 +108,28 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= -github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= -github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8= +github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= 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= @@ -139,8 +149,8 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -151,10 +161,9 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -163,28 +172,31 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -203,39 +215,53 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924 h1:6wqsOpDXA0ZMEswN7f8hX04Y3+gXva7p5emXThtJVlI= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924/go.mod h1:v0O0Au8RE00Z89QxBE6I2q9bR9r3+RO1gLD3oaO2WB0= -github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= -github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6 h1:hms02zQQ0BPcp9CBwh/xda5KwJWdU0IIA/yjtwyRoA4= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6/go.mod h1:jueIfDkkRexwGgLbVB7vGCZlNtd383zuwi4uHHwcbqc= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6 h1:ucHu2bPDT/58AzSgnPDyp4IjnjVbrVWYD3bG5jCbXMY= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 h1:iljEJss3WOwcsMkWy72Yn2zvjw7Gyxc+RXL7r8YKM6g= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= -github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b h1:36knUpKHHAZ86K4FGWXtx8i/EQftGdk2bqCoEu/Cha8= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 h1:W0HKHO8eE8BckTRnhSdqjHKbJcnk068nEWYnWRu6tJY= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= +github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 h1:NJdGFhzT6zMaTod4QkBqVD2sg0I25iw1boOYtTpEwRo= +github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe h1:MDnY5wQbWTpFdDnMRicEnoMfSP5nM/KncARr4skP1ug= +github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe/go.mod h1:z7lx7wI3XZ4u9kmUtAVdwn1BCC9T8aieWSDcuDgPTdQ= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 h1:/vkKPJoweLkRd56V4YHGRAtTG4+/JAlgklGEfvH6l4c= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 h1:LQy2j2+TdKLSWsUTUYuqmQPn8kjqCLjGI3ZJYGtDc08= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= -github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74= -github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd h1:ksFjz3ytjK4kH5HFHpLKzDS0/9gmeSuvii1rs8FlxrI= +github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -245,10 +271,10 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -258,31 +284,31 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= @@ -306,10 +332,14 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -324,8 +354,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -360,12 +388,9 @@ golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= -golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -383,14 +408,10 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -399,17 +420,17 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/cron/main.go b/cron/main.go index a085c0391..68ecac221 100644 --- a/cron/main.go +++ b/cron/main.go @@ -1,20 +1,89 @@ +// Command cron runs the cron trigger capability as its own binary. +// +// It hosts no node of its own: the capabilities registry it announces itself to, +// and the settings its limits resolve against, come from the crecore process it +// is pointed at, and what is left here is the capability - the scheduler that +// fires a workflow's trigger on time, and the server that makes it registerable. package main import ( - "github.com/smartcontractkit/capabilities/cron/trigger" - "github.com/smartcontractkit/capabilities/libs/loopserver" + "context" + "log" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" + "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron/server" - "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/capabilities/cron/protos" + "github.com/smartcontractkit/capabilities/cron/trigger" + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/capabilities/libs/standalone/capability" + standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" ) func main() { - loopserver.ServeNew(trigger.ServiceName, func(s *loop.Server) loop.StandardCapabilities { - triggerService, err := trigger.NewTriggerService(s.Logger, nil, s.LimitsFactory) + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + cfg := defaultConfig + + root := &cobra.Command{ + Use: "cron", + Short: "The CRE cron trigger capability", + Long: `Runs the cron trigger capability, which fires a workflow's trigger on the +schedule that workflow registered. + +It holds no keys and no peer: --capabilities.proxy-url is the node's registry, +which this binary announces the capability to and reads its settings from. + +Settings can come from flags, from CRE_/CL_ env vars, or from a --config file; +run "docs" to write the full reference to docs/CONFIG.md.`, + } + root.PersistentFlags().String("config", "", "Path to config file") + + opts := flags.DefaultTOMLOptions("CRE", "CL") + opts.Namespace = "cron" + if err := flags.RegisterCommandFlags(root, &cfg, opts); err != nil { + return err + } + + bootstrapper := standalone.NewBootstrapper(root, standalone.WithOtelViews(trigger.MetricViews())) + lggr := bootstrapper.Logger() + + capDep := capability.Dependency(lggr.Named("Capabilities"), standalonegrpc.FactoryDependency(lggr.Named("CapabilityAPI"))) + + return standalone.Run1(bootstrapper, func( + ctx context.Context, + scfg *standalone.StandaloneConfig, + deps capability.Dependencies, + ) []services.Service { + lggr := scfg.Logger.Named("cron") + + // The real clock: nil is the constructor's "a test drives this one". + triggerService, err := trigger.NewTriggerService(lggr, nil, cfg.Config, trigger.Dependencies{ + LimitsFactory: deps.LimitsFactory, + }) if err != nil { - s.Logger.Fatalw("Failed to create cron trigger service", "error", err) + lggr.Fatalw("Failed to create cron trigger service", "error", err) } - return server.NewCronServer(triggerService) - }, loop.WithOtelViews(trigger.MetricViews())) + // Run supervises the capability and makes it reachable: registered, + // served, and announced to the node's registry. + svcs, err := capability.Run(deps, *scfg, protos.NewCronServer(triggerService)) + if err != nil { + lggr.Fatalw("Failed to host CronCapability", "error", err) + } + return svcs + }, capDep) +} + +// Config is what this binary needs that its host cannot tell it. +type Config struct { + trigger.Config `toml:",inline"` } + +var defaultConfig = Config{} diff --git a/cron/protos/gen/main.go b/cron/protos/gen/main.go new file mode 100644 index 000000000..eac42832b --- /dev/null +++ b/cron/protos/gen/main.go @@ -0,0 +1,25 @@ +// Command gen generates the cron capability's protos. +// +// It lives here, rather than in the module holding the generator, so that it is +// built from the generator - and the protoc plugins - that this capability's +// go.mod pins. Updating those is then this capability's own change, and a +// capability that has not made it keeps generating exactly what it did before. +package main + +import ( + "fmt" + "os" + + "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/generator" +) + +//go:generate go run . + +func main() { + // Any capability whose protos these import is named here, so that its + // protos are compiled alongside and linked to the Go code it generated. + if err := generator.Generate(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cron/protos/trigger.pb.go b/cron/protos/trigger.pb.go new file mode 100644 index 000000000..db3205d6d --- /dev/null +++ b/cron/protos/trigger.pb.go @@ -0,0 +1,228 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: capabilities/scheduler/cron/v1/trigger.proto + +package protos + +import ( + _ "github.com/smartcontractkit/chainlink-protos/cre/go/tools/generator" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Schedule string `protobuf:"bytes,1,opt,name=schedule,proto3" json:"schedule,omitempty"` // Cron schedule string + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_capabilities_scheduler_cron_v1_trigger_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetSchedule() string { + if x != nil { + return x.Schedule + } + return "" +} + +type Payload struct { + state protoimpl.MessageState `protogen:"open.v1"` + ScheduledExecutionTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=scheduled_execution_time,json=scheduledExecutionTime,proto3" json:"scheduled_execution_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Payload) Reset() { + *x = Payload{} + mi := &file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Payload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Payload) ProtoMessage() {} + +func (x *Payload) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Payload.ProtoReflect.Descriptor instead. +func (*Payload) Descriptor() ([]byte, []int) { + return file_capabilities_scheduler_cron_v1_trigger_proto_rawDescGZIP(), []int{1} +} + +func (x *Payload) GetScheduledExecutionTime() *timestamppb.Timestamp { + if x != nil { + return x.ScheduledExecutionTime + } + return nil +} + +// Deprecated: Marked as deprecated in capabilities/scheduler/cron/v1/trigger.proto. +type LegacyPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + ScheduledExecutionTime string `protobuf:"bytes,1,opt,name=scheduled_execution_time,json=scheduledExecutionTime,proto3" json:"scheduled_execution_time,omitempty"` // Time that cron trigger's task execution had been scheduled to occur (RFC3339Nano formatted) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LegacyPayload) Reset() { + *x = LegacyPayload{} + mi := &file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LegacyPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LegacyPayload) ProtoMessage() {} + +func (x *LegacyPayload) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LegacyPayload.ProtoReflect.Descriptor instead. +func (*LegacyPayload) Descriptor() ([]byte, []int) { + return file_capabilities_scheduler_cron_v1_trigger_proto_rawDescGZIP(), []int{2} +} + +func (x *LegacyPayload) GetScheduledExecutionTime() string { + if x != nil { + return x.ScheduledExecutionTime + } + return "" +} + +var File_capabilities_scheduler_cron_v1_trigger_proto protoreflect.FileDescriptor + +const file_capabilities_scheduler_cron_v1_trigger_proto_rawDesc = "" + + "\n" + + ",capabilities/scheduler/cron/v1/trigger.proto\x12\x1ecapabilities.scheduler.cron.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a*tools/generator/v1alpha/cre_metadata.proto\"$\n" + + "\x06Config\x12\x1a\n" + + "\bschedule\x18\x01 \x01(\tR\bschedule\"_\n" + + "\aPayload\x12T\n" + + "\x18scheduled_execution_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x16scheduledExecutionTime\"M\n" + + "\rLegacyPayload\x128\n" + + "\x18scheduled_execution_time\x18\x01 \x01(\tR\x16scheduledExecutionTime:\x02\x18\x012\xf5\x01\n" + + "\x04Cron\x12\\\n" + + "\aTrigger\x12&.capabilities.scheduler.cron.v1.Config\x1a'.capabilities.scheduler.cron.v1.Payload0\x01\x12s\n" + + "\rLegacyTrigger\x12&.capabilities.scheduler.cron.v1.Config\x1a-.capabilities.scheduler.cron.v1.LegacyPayload\"\t\x8a\xb5\x18\x02\b\x01\x88\x02\x010\x01\x1a\x1a\x82\xb5\x18\x16\b\x01\x12\x12cron-trigger@1.0.0b\x06proto3" + +var ( + file_capabilities_scheduler_cron_v1_trigger_proto_rawDescOnce sync.Once + file_capabilities_scheduler_cron_v1_trigger_proto_rawDescData []byte +) + +func file_capabilities_scheduler_cron_v1_trigger_proto_rawDescGZIP() []byte { + file_capabilities_scheduler_cron_v1_trigger_proto_rawDescOnce.Do(func() { + file_capabilities_scheduler_cron_v1_trigger_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_capabilities_scheduler_cron_v1_trigger_proto_rawDesc), len(file_capabilities_scheduler_cron_v1_trigger_proto_rawDesc))) + }) + return file_capabilities_scheduler_cron_v1_trigger_proto_rawDescData +} + +var file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_capabilities_scheduler_cron_v1_trigger_proto_goTypes = []any{ + (*Config)(nil), // 0: capabilities.scheduler.cron.v1.Config + (*Payload)(nil), // 1: capabilities.scheduler.cron.v1.Payload + (*LegacyPayload)(nil), // 2: capabilities.scheduler.cron.v1.LegacyPayload + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp +} +var file_capabilities_scheduler_cron_v1_trigger_proto_depIdxs = []int32{ + 3, // 0: capabilities.scheduler.cron.v1.Payload.scheduled_execution_time:type_name -> google.protobuf.Timestamp + 0, // 1: capabilities.scheduler.cron.v1.Cron.Trigger:input_type -> capabilities.scheduler.cron.v1.Config + 0, // 2: capabilities.scheduler.cron.v1.Cron.LegacyTrigger:input_type -> capabilities.scheduler.cron.v1.Config + 1, // 3: capabilities.scheduler.cron.v1.Cron.Trigger:output_type -> capabilities.scheduler.cron.v1.Payload + 2, // 4: capabilities.scheduler.cron.v1.Cron.LegacyTrigger:output_type -> capabilities.scheduler.cron.v1.LegacyPayload + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_capabilities_scheduler_cron_v1_trigger_proto_init() } +func file_capabilities_scheduler_cron_v1_trigger_proto_init() { + if File_capabilities_scheduler_cron_v1_trigger_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_capabilities_scheduler_cron_v1_trigger_proto_rawDesc), len(file_capabilities_scheduler_cron_v1_trigger_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_capabilities_scheduler_cron_v1_trigger_proto_goTypes, + DependencyIndexes: file_capabilities_scheduler_cron_v1_trigger_proto_depIdxs, + MessageInfos: file_capabilities_scheduler_cron_v1_trigger_proto_msgTypes, + }.Build() + File_capabilities_scheduler_cron_v1_trigger_proto = out.File + file_capabilities_scheduler_cron_v1_trigger_proto_goTypes = nil + file_capabilities_scheduler_cron_v1_trigger_proto_depIdxs = nil +} diff --git a/cron/protos/trigger.proto b/cron/protos/trigger.proto new file mode 100644 index 000000000..d0efaf3f8 --- /dev/null +++ b/cron/protos/trigger.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package capabilities.scheduler.cron.v1; + +import "google/protobuf/timestamp.proto"; +import "tools/generator/v1alpha/cre_metadata.proto"; + +message Config { + string schedule = 1; // Cron schedule string +} + +message Payload { + google.protobuf.Timestamp scheduled_execution_time = 1; +} + +message LegacyPayload { + option deprecated = true; + string scheduled_execution_time = 1; // Time that cron trigger's task execution had been scheduled to occur (RFC3339Nano formatted) +} + +service Cron { + option (tools.generator.v1alpha.capability) = { + mode: MODE_DON + capability_id: "cron-trigger@1.0.0" + }; + + rpc Trigger(Config) returns (stream Payload); + + rpc LegacyTrigger(Config) returns (stream LegacyPayload) { + option (tools.generator.v1alpha.method) = {map_to_untyped_api: true}; + option deprecated = true; + } +} diff --git a/cron/protos/trigger_server_gen.go b/cron/protos/trigger_server_gen.go new file mode 100644 index 000000000..9e48d22d2 --- /dev/null +++ b/cron/protos/trigger_server_gen.go @@ -0,0 +1,139 @@ +// Code generated by github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/protoc, DO NOT EDIT. + +package protos + +import ( + "context" + "fmt" + + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" +) + +// Avoid unused imports if there is configuration type +var _ = emptypb.Empty{} + +// CronCapability is what a capability implements to be served as cron-trigger@1.0.0. +// +// It carries no Initialise: a capability is given what it needs when it is +// built, and everything a host does to it - registering it, serving it, +// announcing it, taking it back out - belongs to the bootstrapper that hosts it +// rather than to the capability or to this server. +type CronCapability interface { + RegisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *Config) (<-chan capabilities.TriggerAndId[*Payload], caperrors.Error) + UnregisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *Config) caperrors.Error + + RegisterLegacyTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *Config) (<-chan capabilities.TriggerAndId[*LegacyPayload], caperrors.Error) + UnregisterLegacyTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *Config) caperrors.Error + AckEvent(ctx context.Context, triggerId string, eventId string, method string) caperrors.Error + + Start(ctx context.Context) error + Close() error + HealthReport() map[string]error + Name() string + Description() string + Ready() error +} + +func NewCronServer(capability CronCapability) *CronServer { + stopCh := make(chan struct{}) + return &CronServer{ + cronCapability: cronCapability{CronCapability: capability, stopCh: stopCh}, + stopCh: stopCh, + } +} + +// CronServer serves the capability: it turns the untyped requests a host +// delivers into calls on the typed methods above, and is itself the +// capabilities.ExecutableAndTriggerCapability a host registers and serves. +type CronServer struct { + cronCapability + stopCh chan struct{} +} + +// Close stops answering registered triggers, then closes the capability. +// +// Nothing is deregistered here. What put this capability in a registry, and +// announced it to a node, is the host - so taking it back out is the host's too, +// and doing it from both would race a shutdown against itself. +func (c *CronServer) Close() error { + if c.stopCh != nil { + close(c.stopCh) + } + + return c.cronCapability.Close() +} + +type cronCapability struct { + CronCapability + stopCh chan struct{} +} + +func (c *cronCapability) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { + return capabilities.NewCapabilityInfo("cron-trigger@1.0.0", capabilities.CapabilityTypeCombined, c.CronCapability.Description()) +} + +var _ capabilities.ExecutableAndTriggerCapability = (*cronCapability)(nil) + +const CronID = "cron-trigger@1.0.0" + +func (c *cronCapability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + case "Trigger": + input := &Config{} + return capabilities.RegisterTrigger(ctx, c.stopCh, "cron-trigger@1.0.0", request, input, c.CronCapability.RegisterTrigger) + case "": + input := &Config{} + return capabilities.RegisterTrigger(ctx, c.stopCh, "cron-trigger@1.0.0", request, input, c.CronCapability.RegisterLegacyTrigger) + default: + return nil, fmt.Errorf("trigger %s not found", request.Method) + } +} + +func (c *cronCapability) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error { + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + case "Trigger": + input := &Config{} + _, err := capabilities.FromValueOrAny(request.Config, request.Payload, input) + if err != nil { + return err + } + return c.CronCapability.UnregisterTrigger(ctx, request.TriggerID, request.Metadata, input) + case "": + input := &Config{} + _, err := capabilities.FromValueOrAny(request.Config, request.Payload, input) + if err != nil { + return err + } + return c.CronCapability.UnregisterLegacyTrigger(ctx, request.TriggerID, request.Metadata, input) + default: + return fmt.Errorf("method %s not found", request.Method) + } +} + +func (c *cronCapability) AckEvent(ctx context.Context, triggerId string, eventId string, method string) error { + switch method { + case "Trigger": + return c.CronCapability.AckEvent(ctx, triggerId, eventId, method) + case "": + return c.CronCapability.AckEvent(ctx, triggerId, eventId, method) + default: + return fmt.Errorf("trigger %s not found", method) + } +} + +func (c *cronCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (c *cronCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} + +func (c *cronCapability) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + return capabilities.CapabilityResponse{}, fmt.Errorf("method %s not found", request.Method) +} diff --git a/cron/trigger/trigger.go b/cron/trigger/trigger.go index d78adf404..a453e5f06 100644 --- a/cron/trigger/trigger.go +++ b/cron/trigger/trigger.go @@ -2,7 +2,6 @@ package trigger import ( "context" - "encoding/json" "errors" "fmt" "runtime/debug" @@ -14,11 +13,11 @@ import ( "github.com/jonboulle/clockwork" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/smartcontractkit/capabilities/cron/protos" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/cron" - crontypedapi "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron/server" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/custmsg" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -26,7 +25,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services/orgresolver" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" "github.com/smartcontractkit/chainlink-common/pkg/workflows" "github.com/smartcontractkit/chainlink-common/pkg/workflows/events" ) @@ -36,13 +34,32 @@ const ServiceName = "CronCapabilities" const defaultSendChannelBufferSize = 1000 var cronTriggerInfo = capabilities.MustNewCapabilityInfo( - server.CronID, + protos.CronID, capabilities.CapabilityTypeTrigger, "A trigger that uses a cron schedule to run periodically at fixed times, dates, or intervals.", ) +// Config is what this capability needs that its host cannot tell it. type Config struct { - FastestScheduleIntervalSeconds int `json:"fastestScheduleIntervalSeconds"` + FastestScheduleIntervalSeconds int `json:"fastestScheduleIntervalSeconds" usage:"fastest cron schedule a workflow may register, in seconds; 0 keeps the CRE default"` +} + +// Dependencies are what a cron trigger needs from wherever it is hosted: the +// limits the schedules it accepts are bounded by, and who owns the workflow a +// trigger fires for. +// +// They are taken here rather than through an Initialise the host calls, because +// a capability that is not yet usable is only a way to be used too early: with +// these, what NewTriggerService returns is ready, and Start runs it. +type Dependencies struct { + // LimitsFactory resolves the CRE settings this capability enforces: how fast + // a schedule may be, and whether multi-trigger execution IDs are on. + LimitsFactory limits.Factory + + // OrgResolver names the organisation a workflow's owner belongs to, for the + // events a firing trigger emits. Optional: without it those events carry no + // organisation ID. + OrgResolver orgresolver.OrgResolver } type Response struct { @@ -71,12 +88,12 @@ type Service struct { orgResolver orgresolver.OrgResolver } -func (s *Service) RegisterLegacyTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *crontypedapi.Config) (<-chan capabilities.TriggerAndId[*crontypedapi.LegacyPayload], caperrors.Error) { //nolint:staticcheck +func (s *Service) RegisterLegacyTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *protos.Config) (<-chan capabilities.TriggerAndId[*protos.LegacyPayload], caperrors.Error) { //nolint:staticcheck ch, err := s.RegisterTrigger(ctx, triggerID, metadata, input) if err != nil { return nil, err } - mapped := make(chan capabilities.TriggerAndId[*crontypedapi.LegacyPayload]) //nolint + mapped := make(chan capabilities.TriggerAndId[*protos.LegacyPayload]) //nolint go func() { defer close(mapped) for { @@ -87,9 +104,9 @@ func (s *Service) RegisterLegacyTrigger(ctx context.Context, triggerID string, m if !ok { return } - mapped <- capabilities.TriggerAndId[*crontypedapi.LegacyPayload]{ //nolint:staticcheck + mapped <- capabilities.TriggerAndId[*protos.LegacyPayload]{ //nolint:staticcheck Id: triggerEvent.Id, - Trigger: &crontypedapi.LegacyPayload{ //nolint:staticcheck + Trigger: &protos.LegacyPayload{ //nolint:staticcheck ScheduledExecutionTime: triggerEvent.Trigger.ScheduledExecutionTime.AsTime().Format(time.RFC3339Nano), }, } @@ -99,15 +116,21 @@ func (s *Service) RegisterLegacyTrigger(ctx context.Context, triggerID string, m return mapped, nil } -func (s *Service) UnregisterLegacyTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *crontypedapi.Config) caperrors.Error { +func (s *Service) UnregisterLegacyTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *protos.Config) caperrors.Error { return s.UnregisterTrigger(ctx, triggerID, metadata, input) } var _ services.Service = &Service{} +var _ protos.CronCapability = &Service{} + // NewTriggerService creates a new trigger service. Optionally, a clock can be passed in for testing, if nil -// the system clock will be used. The orgResolver is optional and can be nil, but should be set in live environments. -func NewTriggerService(parentLggr logger.Logger, clock clockwork.Clock, limitsFactory limits.Factory) (*Service, error) { +// the system clock will be used. +// +// The limiters are made here rather than when the service starts: they are what +// decides whether a registration is allowed at all, so a service that has them +// is one that can answer, and Start only has to schedule. +func NewTriggerService(parentLggr logger.Logger, clock clockwork.Clock, cfg Config, deps Dependencies) (*Service, error) { lggr := logger.Named(parentLggr, "CRONTrigger") metrics, err := NewMetrics() @@ -115,6 +138,24 @@ func NewTriggerService(parentLggr logger.Logger, clock clockwork.Clock, limitsFa return nil, fmt.Errorf("error creating metrics: %w", err) } + limit := cresettings.Default.PerWorkflow.CRONTrigger.FastestScheduleInterval // copy + if cfg.FastestScheduleIntervalSeconds > 0 { + limit.DefaultValue = time.Duration(cfg.FastestScheduleIntervalSeconds) * time.Second + } + fastestScheduleInterval, err := deps.LimitsFactory.MakeTimeLimiter(limit) + if err != nil { + return nil, fmt.Errorf("failed to create limiter: %w", err) + } + + multiTriggerFlag, err := limits.MakeRangeLimiter(deps.LimitsFactory, cresettings.Default.PerWorkflow.FeatureMultiTriggerExecutionIDsActivePeriod) + if err != nil { + return nil, fmt.Errorf("failed to create rangelimiter: %w", err) + } + + if deps.OrgResolver == nil { + lggr.Warn("OrgResolver is nil, cron capability will not be able to fetch organization ID") + } + var options []gocron.SchedulerOption options = append(options, gocron.WithMonitor(NewCronMonitor(metrics))) // Set scheduler location to UTC for consistency across nodes. @@ -134,12 +175,15 @@ func NewTriggerService(parentLggr logger.Logger, clock clockwork.Clock, limitsFa } return &Service{ - lggr: lggr, - CapabilityInfo: cronTriggerInfo, - limitsFactory: limitsFactory, - triggers: NewCronStore(), - scheduler: scheduler, - clock: clock, + lggr: lggr, + CapabilityInfo: cronTriggerInfo, + limitsFactory: deps.LimitsFactory, + fastestScheduleInterval: fastestScheduleInterval, + multiTriggerFlag: multiTriggerFlag, + orgResolver: deps.OrgResolver, + triggers: NewCronStore(), + scheduler: scheduler, + clock: clock, labeler: custmsg.NewLabeler().With( "capabilityID", cronTriggerInfo.ID, "capabilityVersion", cronTriggerInfo.Version(), @@ -149,46 +193,7 @@ func NewTriggerService(parentLggr logger.Logger, clock clockwork.Clock, limitsFa }, nil } -func (s *Service) Initialise(ctx context.Context, dependencies core.StandardCapabilitiesDependencies) error { - s.lggr.Debugw("Initialising cron trigger capability", "serviceName", ServiceName) - - var cronConfig Config - if len(dependencies.Config) > 0 { - err := json.Unmarshal([]byte(dependencies.Config), &cronConfig) - if err != nil { - return fmt.Errorf("failed to unmarshal config: %s %w", dependencies.Config, err) - } - } - - limit := cresettings.Default.PerWorkflow.CRONTrigger.FastestScheduleInterval // copy - if cronConfig.FastestScheduleIntervalSeconds > 0 { - limit.DefaultValue = time.Duration(cronConfig.FastestScheduleIntervalSeconds) * time.Second - } - limiter, err := s.limitsFactory.MakeTimeLimiter(limit) - if err != nil { - return fmt.Errorf("failed to create limiter: %w", err) - } - s.fastestScheduleInterval = limiter - - s.multiTriggerFlag, err = limits.MakeRangeLimiter(s.limitsFactory, cresettings.Default.PerWorkflow.FeatureMultiTriggerExecutionIDsActivePeriod) - if err != nil { - return fmt.Errorf("failed to create rangelimiter: %w", err) - } - - s.orgResolver = dependencies.OrgResolver - if s.orgResolver == nil { - s.lggr.Warn("OrgResolver is nil, cron capability will not be able to fetch organization ID") - } - - err = s.Start(ctx) - if err != nil { - return fmt.Errorf("error when starting trigger service: %w", err) - } - - return nil -} - -func (s *Service) RegisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *crontypedapi.Config) (<-chan capabilities.TriggerAndId[*crontypedapi.Payload], caperrors.Error) { +func (s *Service) RegisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *protos.Config) (<-chan capabilities.TriggerAndId[*protos.Payload], caperrors.Error) { ctx = metadata.ContextWithCRE(ctx) var muCh sync.RWMutex // extra synchronization to prevent the cron task from racing to send on the closed chan and re-register itself // hold the lock until we call triggers.Write @@ -201,7 +206,7 @@ func (s *Service) RegisterTrigger(ctx context.Context, triggerID string, metadat } var job gocron.Job - callbackCh := make(chan capabilities.TriggerAndId[*crontypedapi.Payload], defaultSendChannelBufferSize) + callbackCh := make(chan capabilities.TriggerAndId[*protos.Payload], defaultSendChannelBufferSize) closeCh := func() { muCh.Lock() @@ -376,7 +381,7 @@ func (s *Service) RegisterTrigger(ctx context.Context, triggerID string, metadat return callbackCh, nil } -func createTriggerResponse(scheduledExecutionTime time.Time) capabilities.TriggerAndId[*crontypedapi.Payload] { +func createTriggerResponse(scheduledExecutionTime time.Time) capabilities.TriggerAndId[*protos.Payload] { // Ensure UTC time is used for consistency across nodes. scheduledExecutionTimeUTC := scheduledExecutionTime.UTC() @@ -386,8 +391,8 @@ func createTriggerResponse(scheduledExecutionTime time.Time) capabilities.Trigge scheduledExecutionTimeFormatted := scheduledExecutionTimeUTC.Format(time.RFC3339) triggerEventID := scheduledExecutionTimeFormatted - return capabilities.TriggerAndId[*crontypedapi.Payload]{ - Trigger: &crontypedapi.Payload{ + return capabilities.TriggerAndId[*protos.Payload]{ + Trigger: &protos.Payload{ ScheduledExecutionTime: timestamppb.New(scheduledExecutionTimeUTC), }, Id: triggerEventID, @@ -398,7 +403,7 @@ func (s *Service) AckEvent(ctx context.Context, triggerID string, eventID string return nil } -func (s *Service) UnregisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *crontypedapi.Config) caperrors.Error { +func (s *Service) UnregisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *protos.Config) caperrors.Error { trigger, ok := s.triggers.Read(triggerID) if !ok { s.lggr.Warnw("trigger not found", "triggerID", triggerID) diff --git a/cron/trigger/trigger_test.go b/cron/trigger/trigger_test.go index 49aca17d9..6add7928c 100644 --- a/cron/trigger/trigger_test.go +++ b/cron/trigger/trigger_test.go @@ -2,7 +2,6 @@ package trigger import ( "context" - "encoding/json" "errors" "fmt" "math" @@ -19,17 +18,16 @@ import ( "go.uber.org/zap" "google.golang.org/protobuf/types/known/anypb" + "github.com/smartcontractkit/capabilities/cron/protos" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers/cron" - crontypedapi "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron/server" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services/orgresolver" "github.com/smartcontractkit/chainlink-common/pkg/settings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" "github.com/smartcontractkit/chainlink-common/pkg/workflows" "github.com/smartcontractkit/chainlink-protos/cre/go/values" ) @@ -78,7 +76,7 @@ func registerTriggerToCronTriggerService( } if useTypedAPI { - payload, err := anypb.New(&crontypedapi.Config{Schedule: schedule}) + payload, err := anypb.New(&protos.Config{Schedule: schedule}) require.NoError(t, err) request := capabilities.TriggerRegistrationRequest{ @@ -111,11 +109,11 @@ func upwrapCronTriggerEvent(t *testing.T, event capabilities.TriggerEvent, useTypedAPI bool) Response { response := Response{} response.TriggerType = event.TriggerType - assert.Equal(t, server.CronID, response.TriggerType) + assert.Equal(t, protos.CronID, response.TriggerType) response.ID = event.ID if useTypedAPI { - payload := &crontypedapi.LegacyPayload{} //nolint:staticcheck + payload := &protos.LegacyPayload{} //nolint:staticcheck err := event.Payload.UnmarshalTo(payload) require.NoError(t, err) response.Payload = cron.Payload{ScheduledExecutionTime: payload.ScheduledExecutionTime} @@ -252,17 +250,11 @@ func successWithStandardCronIntervals(t *testing.T, useTypedAPI bool) { } } - config, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) - - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(config), - }) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) + require.NoError(t, ts.Start(t.Context())) - triggerAPI := server.NewCronServer(ts) + triggerAPI := protos.NewCronServer(ts) // Register trigger callback, registerUnregisterRequest, err := registerTriggerToCronTriggerService( @@ -333,17 +325,11 @@ func TestCronTrigger_Load(t *testing.T) { fakeClock := clockwork.NewRealClock() - config, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) - - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(config), - }) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) + require.NoError(t, ts.Start(t.Context())) - triggerAPI := server.NewCronServer(ts) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() @@ -481,16 +467,11 @@ func TestCronTrigger_RegisterTriggerBeforeStart_UntypedAPI(t *testing.T) { func testCronTriggerRegisterTriggerBeforeStart(t *testing.T, useTypedAPI bool) { fakeClock := clockwork.NewRealClock() - config, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(config), - }) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) + require.NoError(t, ts.Start(t.Context())) - triggerAPI := server.NewCronServer(ts) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() @@ -554,15 +535,10 @@ func testCronTriggerTimeWindows(t *testing.T, useTypedAPI bool) { fakeClock.Advance(time.Duration(49-minimum) * time.Minute) fakeClock.Advance(time.Duration(8-hour) * time.Hour) - config, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(config), - }) - require.NoError(t, err) - triggerAPI := server.NewCronServer(ts) + require.NoError(t, ts.Start(t.Context())) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() @@ -630,15 +606,10 @@ func testCronTriggerMultipleDifferentSchedules(t *testing.T, useTypedAPI bool) { if fakeClock.Now().Second()%2 == 1 { fakeClock.Advance(time.Second) } - config, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(config), - }) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) - triggerAPI := server.NewCronServer(ts) + require.NoError(t, ts.Start(t.Context())) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() callback1, registerUnregisterRequest1, err := registerTriggerToCronTriggerService( @@ -753,15 +724,10 @@ func testCronTriggerTimeZone(t *testing.T, useTypedAPI bool) { fakeClock.Advance(time.Duration(49-minimum) * time.Minute) fakeClock.Advance(time.Duration(23-hour) * time.Hour) - config, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(config), - }) - require.NoError(t, err) - triggerAPI := server.NewCronServer(ts) + require.NoError(t, ts.Start(t.Context())) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() // Register trigger @@ -868,11 +834,10 @@ func testCronTriggerRegisterTrigger(t *testing.T, useTypedAPI bool) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { fakeClock := clockwork.NewRealClock() - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{}) - require.NoError(t, err) - triggerAPI := server.NewCronServer(ts) + require.NoError(t, ts.Start(t.Context())) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() _, _, err = registerTriggerToCronTriggerService( ctx, @@ -904,16 +869,11 @@ func testCronTriggerRegisterTrigger(t *testing.T, useTypedAPI bool) { } func TestCronTrigger_RegisterTriggerDuplicateError(t *testing.T) { - triggerConfig, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) fakeClock := clockwork.NewRealClock() - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(triggerConfig), - }) - require.NoError(t, err) - triggerAPI := server.NewCronServer(ts) + require.NoError(t, ts.Start(t.Context())) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() @@ -939,16 +899,11 @@ func TestCronTrigger_RegisterTriggerDuplicateError(t *testing.T) { } func TestCronTrigger_UnregisterTriggerError(t *testing.T) { - triggerConfig, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) fakeClock := clockwork.NewRealClock() - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(triggerConfig), - }) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) - triggerAPI := server.NewCronServer(ts) + require.NoError(t, ts.Start(t.Context())) + triggerAPI := protos.NewCronServer(ts) t.Run("OK if trigger not found", func(t *testing.T) { ctx := t.Context() @@ -1021,14 +976,11 @@ func TestCronTrigger_UnregisterTriggerError(t *testing.T) { }) t.Run("NOK fails to unregister if closed", func(t *testing.T) { - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(triggerConfig), - }) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) + require.NoError(t, ts.Start(t.Context())) - triggerAPI := server.NewCronServer(ts) + triggerAPI := protos.NewCronServer(ts) ctx := t.Context() config, err := values.NewMap(map[string]any{ @@ -1059,7 +1011,7 @@ func TestCronTrigger_UnregisterTriggerError(t *testing.T) { func TestCronTrigger_CloseStartErrors(t *testing.T) { fakeClock := clockwork.NewRealClock() - ts, err := NewTriggerService(logger.Nop(), fakeClock, limits.Factory{}) + ts, err := NewTriggerService(logger.Nop(), fakeClock, Config{}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) ctx := t.Context() @@ -1082,17 +1034,12 @@ func (c *panicOnNowClock) Now() time.Time { func TestGocronNewTaskPanic(t *testing.T) { fakeClock := clockwork.NewFakeClock() - config, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) logger, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - ts, err := NewTriggerService(logger, fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{ - Config: string(config), - }) + ts, err := NewTriggerService(logger, fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) + require.NoError(t, ts.Start(t.Context())) - triggerAPI := server.NewCronServer(ts) + triggerAPI := protos.NewCronServer(ts) _, _, err = registerTriggerToCronTriggerService( t.Context(), @@ -1181,13 +1128,9 @@ func TestCronTrigger_MultiTriggerFlag_ExecutionIDPaths(t *testing.T) { fakeClock := clockwork.NewFakeClockAt(startTime) lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - triggerConfig, err := json.Marshal(Config{FastestScheduleIntervalSeconds: 1}) - require.NoError(t, err) - - ts, err := NewTriggerService(lggr, fakeClock, limits.Factory{}) - require.NoError(t, err) - err = ts.Initialise(t.Context(), core.StandardCapabilitiesDependencies{Config: string(triggerConfig)}) + ts, err := NewTriggerService(lggr, fakeClock, Config{FastestScheduleIntervalSeconds: 1}, Dependencies{LimitsFactory: limits.Factory{}}) require.NoError(t, err) + require.NoError(t, ts.Start(t.Context())) if flagActive { // [0, MaxInt64] always contains the fake clock's time (2024-01-01...). @@ -1207,7 +1150,7 @@ func TestCronTrigger_MultiTriggerFlag_ExecutionIDPaths(t *testing.T) { WorkflowID: testWorkflowID, ReferenceID: testReferenceID, } - ch, capErr := ts.RegisterTrigger(t.Context(), testTriggerID, metadata, &crontypedapi.Config{Schedule: everySecond}) + ch, capErr := ts.RegisterTrigger(t.Context(), testTriggerID, metadata, &protos.Config{Schedule: everySecond}) require.Nil(t, capErr) fakeClock.Advance(time.Second + time.Millisecond) @@ -1246,7 +1189,7 @@ func TestCronTrigger_MultiTriggerFlag_ExecutionIDPaths(t *testing.T) { require.Equal(t, expectedExecID, execIDFromLog, "execution ID should match expected hash function") require.Equal(t, !flagActive, isLegacyFromLog, "isLegacyExecutionID should reflect which path was taken") - require.Nil(t, ts.UnregisterTrigger(t.Context(), testTriggerID, metadata, &crontypedapi.Config{Schedule: everySecond})) + require.Nil(t, ts.UnregisterTrigger(t.Context(), testTriggerID, metadata, &protos.Config{Schedule: everySecond})) require.NoError(t, ts.Close()) } From a939bc567968af05a8a928f9549fc7d51d6f0487 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 20 Aug 2026 12:45:23 -0400 Subject: [PATCH 23/30] Cleanup registry and embedding --- consensus/go.mod | 87 ----- consensus/go.sum | 401 --------------------- consensus/main.go | 21 +- crecore/dispatcher_service.go | 14 +- crecore/go.sum | 2 - crecore/main.go | 6 +- crecore/pingpong/main.go | 103 ------ crecore/pingpong/run_4.sh | 126 ------- crecore/pingpong/service.go | 225 ------------ crecore/pingpong/setup/main.go | 96 ----- crecore/proxy_service.go | 6 +- libs/go.sum | 4 - libs/standalone/bootstrap_gen.go | 190 +++++----- libs/standalone/bootstrapper.go | 19 +- libs/standalone/capability/dependency.go | 109 +++--- libs/standalone/capability/embed.go | 30 +- libs/standalone/capability/embed_test.go | 56 +++ libs/standalone/capability/ocrconfig.go | 77 ++++ libs/standalone/db/db_dependency.go | 2 +- libs/standalone/db/db_dependency_test.go | 4 +- libs/standalone/gen/bootstrap.go.tmpl | 10 +- libs/standalone/grpc/dependency.go | 34 +- libs/standalone/grpc/dependency_test.go | 31 +- libs/standalone/ocr/dependency.go | 107 +++--- libs/standalone/ocr/embed.go | 126 ++++--- libs/standalone/ocr/embed_config.go | 320 ++++++++++++++++ libs/standalone/ocr/embed_config_test.go | 129 +++++++ libs/standalone/ocr/keyring.go | 154 ++++---- libs/standalone/ocr/proxy.go | 34 +- libs/standalone/ocr/proxy_test.go | 20 +- libs/standalone/rage/embed.go | 68 ++++ libs/standalone/rage/factories.go | 53 +++ libs/standalone/{ocr => rage}/host.go | 46 ++- libs/standalone/{ocr => rage}/host_test.go | 19 +- libs/standalone/rage/keystore.go | 98 +++++ libs/x/registry/registry.go | 5 +- libs/x/registry/server_test.go | 16 +- 37 files changed, 1352 insertions(+), 1496 deletions(-) delete mode 100644 crecore/pingpong/main.go delete mode 100755 crecore/pingpong/run_4.sh delete mode 100644 crecore/pingpong/service.go delete mode 100644 crecore/pingpong/setup/main.go create mode 100644 libs/standalone/capability/embed_test.go create mode 100644 libs/standalone/capability/ocrconfig.go create mode 100644 libs/standalone/ocr/embed_config.go create mode 100644 libs/standalone/ocr/embed_config_test.go create mode 100644 libs/standalone/rage/embed.go create mode 100644 libs/standalone/rage/factories.go rename libs/standalone/{ocr => rage}/host.go (86%) rename libs/standalone/{ocr => rage}/host_test.go (78%) create mode 100644 libs/standalone/rage/keystore.go diff --git a/consensus/go.mod b/consensus/go.mod index be276989e..0b2796db0 100644 --- a/consensus/go.mod +++ b/consensus/go.mod @@ -23,21 +23,7 @@ require ( ) require ( - cosmossdk.io/api v0.7.6 // indirect - cosmossdk.io/collections v0.4.0 // indirect - cosmossdk.io/core v0.11.0 // indirect - cosmossdk.io/depinject v1.1.0 // indirect - cosmossdk.io/errors v1.0.1 // indirect - cosmossdk.io/log v1.4.1 // indirect - cosmossdk.io/math v1.4.0 // indirect - cosmossdk.io/store v1.1.1 // indirect - cosmossdk.io/x/tx v0.13.7 // indirect - filippo.io/bigmod v0.1.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect - filippo.io/nistec v0.0.4 // indirect - github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect - github.com/99designs/keyring v1.2.1 // indirect - github.com/DataDog/zstd v1.5.7 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NethermindEth/juno v0.15.11 // indirect @@ -46,54 +32,23 @@ require ( github.com/XSAM/otelsql v0.42.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect - github.com/blendle/zapdriver v1.3.1 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/buger/jsonparser v1.2.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect - github.com/cockroachdb/errors v1.12.0 // indirect - github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect - github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect - github.com/cockroachdb/pebble v1.1.5 // indirect - github.com/cockroachdb/redact v1.1.6 // indirect - github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect - github.com/cometbft/cometbft v0.38.21 // indirect - github.com/cometbft/cometbft-db v0.14.1 // indirect github.com/consensys/gnark-crypto v0.20.1 // indirect - github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-db v1.1.1 // indirect - github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect - github.com/cosmos/cosmos-sdk v0.50.14 // indirect - github.com/cosmos/go-bip39 v1.0.0 // indirect - github.com/cosmos/gogoproto v1.7.0 // indirect - github.com/cosmos/ics23/go v0.11.0 // indirect - github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.9.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect - github.com/dgraph-io/badger/v4 v4.2.0 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/ethereum/go-ethereum v1.17.3 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/gagliardetto/binary v0.8.0 // indirect - github.com/gagliardetto/solana-go v1.13.0 // indirect - github.com/gagliardetto/treeout v0.1.4 // indirect - github.com/getsentry/sentry-go v0.35.1 // indirect github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // 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.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -101,14 +56,8 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.2.5 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect @@ -116,14 +65,9 @@ require ( github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect - github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-metrics v0.5.3 // indirect github.com/hashicorp/go-plugin v1.8.0 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect @@ -134,42 +78,29 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jmhodges/levigo v1.0.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.6 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.12.3 // indirect - github.com/linxGnu/grocksdb v1.8.14 // indirect - github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect - github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.3.0 // indirect - github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.2.0 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect - github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/rs/zerolog v1.33.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect - github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect @@ -182,33 +113,20 @@ require ( github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect - github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect - github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/stellar/go-stellar-sdk v0.4.0 // indirect - github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect - github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.16 // indirect - github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect - github.com/tendermint/go-amino v0.16.0 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xssnick/tonutils-go v1.14.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zondax/hid v0.9.2 // indirect - github.com/zondax/ledger-go v0.14.3 // indirect - go.dedis.ch/fixbuf v1.0.3 // indirect - go.dedis.ch/kyber/v3 v3.1.0 // indirect - go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect @@ -237,17 +155,12 @@ require ( golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/grpc v1.82.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.1 // indirect - pgregory.net/rapid v1.1.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) replace github.com/smartcontractkit/chainlink-common => ../../chainlink-common diff --git a/consensus/go.sum b/consensus/go.sum index 1c8dc5c09..ecb828599 100644 --- a/consensus/go.sum +++ b/consensus/go.sum @@ -1,37 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= -cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= -cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= -cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= -cosmossdk.io/core v0.11.0 h1:vtIafqUi+1ZNAE/oxLOQQ7Oek2n4S48SWLG8h/+wdbo= -cosmossdk.io/core v0.11.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= -cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E= -cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI= -cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= -cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= -cosmossdk.io/log v1.4.1 h1:wKdjfDRbDyZRuWa8M+9nuvpVYxrEOwbD/CA8hvhU8QM= -cosmossdk.io/log v1.4.1/go.mod h1:k08v0Pyq+gCP6phvdI6RCGhLf/r425UT6Rk/m+o74rU= -cosmossdk.io/math v1.4.0 h1:XbgExXFnXmF/CccPPEto40gOO7FpWu9yWNAZPN3nkNQ= -cosmossdk.io/math v1.4.0/go.mod h1:O5PkD4apz2jZs4zqFdTr16e1dcaQCc5z6lkEnrrppuk= -cosmossdk.io/store v1.1.1 h1:NA3PioJtWDVU7cHHeyvdva5J/ggyLDkyH0hGHl2804Y= -cosmossdk.io/store v1.1.1/go.mod h1:8DwVTz83/2PSI366FERGbWSH7hL6sB7HbYp8bqksNwM= -cosmossdk.io/x/tx v0.13.7 h1:8WSk6B/OHJLYjiZeMKhq7DK7lHDMyK0UfDbBMxVmeOI= -cosmossdk.io/x/tx v0.13.7/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= -filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= -filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -filippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s= -filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= -github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= @@ -46,63 +17,33 @@ github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/XSAM/otelsql v0.42.0 h1:Li0xF4eJUxG2e0x3D4rvRlys1f27yJKvjTh7ljkUP5o= github.com/XSAM/otelsql v0.42.0/go.mod h1:4mOrEv+cS1KmKzrvTktvJnstr5GtKSAK+QHvFR9OcpI= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow-go/v18 v18.6.0 h1:GX/Jyd3R7mCLiECAwY9FWbbaYblie2WXBSz4Sw8fNpM= github.com/apache/arrow-go/v18 v18.6.0/go.mod h1:gm3MiPpY82fLYK5VKPB3WoJbsiLVDfT7flD5/vHReKw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= -github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= -github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= -github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -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/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCeN8rge3F/deT52w2WJMmIC5mHNUq+Ut8= github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= -github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A= @@ -115,40 +56,12 @@ github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4 github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 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/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= -github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= -github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= -github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= -github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= -github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= -github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= -github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= -github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.50.14 h1:G8CtGHFWbExa+ZpVOVAb4kFmko/R30igsYOwyzRMtgY= -github.com/cosmos/cosmos-sdk v0.50.14/go.mod h1:hrWEFMU1eoXqLJeE6VVESpJDQH67FS1nnMrQIjO2daw= -github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= -github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= -github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= -github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= -github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= -github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= -github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= -github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= -github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= -github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= -github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= -github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -163,27 +76,12 @@ 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.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= -github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= -github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= -github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= -github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= -github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= @@ -195,48 +93,22 @@ github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSa github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= -github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= -github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= -github.com/gagliardetto/solana-go v1.13.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= -github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= -github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= -github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -256,38 +128,20 @@ github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -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= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= -github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -295,13 +149,11 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -313,26 +165,16 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -343,39 +185,18 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5 github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= -github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= @@ -386,16 +207,10 @@ github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZ github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= -github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= -github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= @@ -418,34 +233,21 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= -github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= -github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -457,23 +259,14 @@ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjS github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= -github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= -github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= -github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -481,13 +274,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= -github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= @@ -495,42 +283,15 @@ github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= -github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= -github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= -github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -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= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= -github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= -github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -539,12 +300,8 @@ github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7ol github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= @@ -556,70 +313,42 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 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= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= -github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= -github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -642,10 +371,6 @@ github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12i github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd h1:ksFjz3ytjK4kH5HFHpLKzDS0/9gmeSuvii1rs8FlxrI= github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= -github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb h1:kLHdQQkijaPGsBbtV2rJgpzVpQ96e7T10pzjNlWfK8U= -github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb/go.mod h1:4s5hj/nlMF9WV+T5Uhy4n9IYpRpzfJzT+vTKkNT7T+Y= -github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= -github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945/go.mod h1:m3pdp17i4bD50XgktkzWetcV5yaLsi7Gunbv4ZgN6qg= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -659,28 +384,17 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stellar/go-stellar-sdk v0.4.0 h1:WBLeJk7EllU7WhwrTH1L5Mu2EhZor/xy5sVi/82JzNQ= -github.com/stellar/go-stellar-sdk v0.4.0/go.mod h1:tLKAQPxa2I5UvGMabBbUXcY3fmgYnfDudrMeK7CDX4w= -github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf h1:GY1RVbX3Hg7poPXEf6yojjP0hyypvgUgZmCqQU9D0xg= -github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf/go.mod h1:If+U9Z1W5xU97VrOgJandQT+2dN7/iOpkCrxBJEyF80= -github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= -github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -690,17 +404,10 @@ github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jq github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= -github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= -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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= -github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -713,32 +420,13 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg= github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= -github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= -github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= -github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= -go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= -go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= -go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= -go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= -go.dedis.ch/kyber/v3 v3.1.0 h1:ghu+kiRgM5JyD9TJ0hTIxTLQlJBR/ehjWvWwYW3XsC0= -go.dedis.ch/kyber/v3 v3.1.0/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U= -go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= -go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= -go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= -go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= -go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= @@ -787,17 +475,10 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= @@ -806,13 +487,9 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -821,100 +498,53 @@ golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5l golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -926,28 +556,19 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= -google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= @@ -956,8 +577,6 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= @@ -974,39 +593,19 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= -nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= -pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/consensus/main.go b/consensus/main.go index 57fc0aa27..0825d802a 100644 --- a/consensus/main.go +++ b/consensus/main.go @@ -14,7 +14,6 @@ import ( "github.com/jonboulle/clockwork" "github.com/spf13/cobra" - "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/config/flags" "github.com/smartcontractkit/chainlink-common/pkg/services" @@ -48,9 +47,9 @@ func run() error { observations a workflow's nodes make and returns the agreed result. It holds no keys and no peer: --ocr.proxy-address is the crecore process hosting -this node's rage identity, which also signs on this capability's behalf, and ---capabilities.proxy-url is the registry that says which DON this is and what -OCR configuration it runs under. +this node's rage identity, which signs on this capability's behalf and serves the +registry saying what OCR configuration this oracle runs under. +--capabilities.proxy-url is where the capabilities this binary hosts are announced. Settings can come from flags, from CRE_/CL_ env vars, or from a --config file; run "docs" to write the full reference to docs/CONFIG.md.`, @@ -81,13 +80,16 @@ run "docs" to write the full reference to docs/CONFIG.md.`, capabilityImpl, err := action.NewConsensusCapability(lggr, clockwork.NewRealClock(), responseCacheExpiry, cfg.ConsensusCapabilityConfig, action.Dependencies{ - DonID: deps.CapabilityDonID, - Registry: deps.OCRConfigRegistry(), + DonID: deps.CapabilityDonID, + // The configuration comes from the registry, whichever form resolved it: the node's + // for a configured run, and one computed over the run's own instances for an embedded + // one. Either way this reads it off the same field and cannot tell them apart. + Registry: deps.OCRConfigRegistry, Endpoints: factories.OCR2Endpoint, Offchain: factories.Offchain, Onchain: factories.Onchain, TransmitAccount: factories.TransmitAccount, - Bootstrappers: cfg.Bootstrappers.ToBootstrapperLocators(), + Bootstrappers: factories.Bootstrappers, LimitsFactory: deps.LimitsFactory, Metrics: scfg.MetricsRegisterer, }) @@ -106,10 +108,11 @@ run "docs" to write the full reference to docs/CONFIG.md.`, } // Config is what this binary needs that its host cannot tell it. +// +// The DON's bootstrap peers are not here: where a peer can be reached is a property of the network +// this process delegates to, so it is configured with that network - see ocr.ProxyConfig. type Config struct { action.ConsensusCapabilityConfig `toml:",inline"` - - Bootstrappers config.BootstrapperLocators `usage:"peerID@host:port of the DON's bootstrap peers" example:"['12D3KooWFirst@127.0.0.1:6690']"` } var defaultConfig = Config{} diff --git a/crecore/dispatcher_service.go b/crecore/dispatcher_service.go index 5efe4af06..90361deb9 100644 --- a/crecore/dispatcher_service.go +++ b/crecore/dispatcher_service.go @@ -11,9 +11,9 @@ import ( commonsrv "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types/core" - "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/capabilities/libs/standalone/rage" don2don "github.com/smartcontractkit/capabilities/libs/x/don2don" - rage "github.com/smartcontractkit/capabilities/libs/x/rage" + xrage "github.com/smartcontractkit/capabilities/libs/x/rage" ) // DispatcherConfig is the subset of don2don.DispatcherConfig a binary fills in from flags. Field @@ -60,14 +60,14 @@ type dispatcherService struct { eng *commonsrv.Engine cfg DispatcherConfig - factories *ocr.RageFactories + factories *rage.Factories registry core.CapabilitiesRegistry lggr logger.Logger } // newDispatcherService builds the service using the standard services.Config/Engine pattern, so // its lifecycle and health integrate with the bootstrapper's aggregated health report. -func newDispatcherService(cfg DispatcherConfig, lggr logger.Logger, factories *ocr.RageFactories, registry core.CapabilitiesRegistry) *dispatcherService { +func newDispatcherService(cfg DispatcherConfig, lggr logger.Logger, factories *rage.Factories, registry core.CapabilitiesRegistry) *dispatcherService { s := &dispatcherService{cfg: cfg, factories: factories, registry: registry, lggr: lggr} s.Service, s.eng = commonsrv.Config{ Name: "Dispatcher", @@ -81,7 +81,7 @@ func (s *dispatcherService) start(ctx context.Context) error { return errors.New("no PeerGroup factory: the ocr dependency did not host a real peer") } - sharedPeer := rage.NewDon2DonSharedPeer(peerSource{s.factories}, nil, s.lggr) + sharedPeer := xrage.NewDon2DonSharedPeer(peerSource{s.factories}, nil, s.lggr) if err := sharedPeer.Start(ctx); err != nil { return err } @@ -104,9 +104,9 @@ func (s *dispatcherService) start(ctx context.Context) error { return nil } -// peerSource adapts ocr.RageFactories's peer group factory and identity into rage.PeerSource. +// peerSource adapts rage.Factories's peer group factory and identity into xrage.PeerSource. type peerSource struct { - factories *ocr.RageFactories + factories *rage.Factories } func (p peerSource) PeerGroupFactory() networking.PeerGroupFactory { return p.factories.PeerGroup } diff --git a/crecore/go.sum b/crecore/go.sum index 535b02118..b3ca061ef 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -712,8 +712,6 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= -github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 h1:JFo7C3FilwhfwGBLAyj2umbL+P4QxGmVi/b8yt9kqvI= diff --git a/crecore/main.go b/crecore/main.go index 6d8d8f3d5..e2c0bb4a3 100644 --- a/crecore/main.go +++ b/crecore/main.go @@ -12,7 +12,7 @@ import ( "github.com/smartcontractkit/capabilities/libs/standalone" "github.com/smartcontractkit/capabilities/libs/standalone/db" standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" - "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/capabilities/libs/standalone/rage" "github.com/smartcontractkit/capabilities/libs/x/registry" "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" @@ -72,7 +72,7 @@ run "docs" to write the full reference to docs/CONFIG.md.`, lggr := bootstrapper.Logger() dbDep := db.Dependency(embeddedMigrations, migrationsTable) - ocrDep := ocr.Host(lggr.Named("OCR"), dbDep, ocrDiscovererTable) + ocrDep := rage.Host(lggr.Named("OCR"), dbDep, ocrDiscovererTable) readerDep := evmregistry.Dependency(lggr.Named("CapabilitiesRegistry"), evm.Dependency(lggr.Named("EVM"))) @@ -81,7 +81,7 @@ run "docs" to write the full reference to docs/CONFIG.md.`, return standalone.Run4(bootstrapper, func( ctx context.Context, scfg *standalone.StandaloneConfig, - factories *ocr.RageFactories, + factories *rage.Factories, reader registry.Reader, database *sql.DB, grpcSrv *standalonegrpc.Server, diff --git a/crecore/pingpong/main.go b/crecore/pingpong/main.go deleted file mode 100644 index 2fd194a95..000000000 --- a/crecore/pingpong/main.go +++ /dev/null @@ -1,103 +0,0 @@ -// Command pingpong is a sample CRE binary: it passes messages between its instances over a libocr -// binary network endpoint, without hosting a rage peer itself. -// -// Each instance delegates its rage networking to a crecore p2p proxy (see the ocr.Proxy bootstrap -// dependency), which hosts the peer whose identity --ocr.peer-id names. So this binary needs no -// database, no keystore and no listen address of its own: it is told which peer it is and where to -// reach the process holding that peer's key, and everything else - dialling, discovery, encryption - -// happens over there. -// -// What it does with the endpoint is deliberately trivial, so that seeing the messages arrive is the -// whole point: oracle 0 says hi to 1, 1 says hi to 2, and so on round to the last, which says hi -// back to 0. Once a lap is complete 0 broadcasts that it is starting again, and the lap repeats. -// Every instance prints each message it receives. -// -// run_4.sh runs four of these against four crecore proxies on one machine. -package main - -import ( - "context" - "log" - "time" - - "github.com/spf13/cobra" - - "github.com/smartcontractkit/chainlink-common/pkg/config" - "github.com/smartcontractkit/chainlink-common/pkg/config/flags" - "github.com/smartcontractkit/chainlink-common/pkg/services" - - "github.com/smartcontractkit/capabilities/libs/standalone" - "github.com/smartcontractkit/capabilities/libs/standalone/ocr" -) - -func main() { - if err := run(); err != nil { - log.Fatal(err) - } -} - -func run() error { - cfg := defaultConfig - - root := &cobra.Command{ - Use: "pingpong", - Short: "Sample app passing messages over a proxied libocr binary network endpoint", - Long: `Passes messages between its instances over a libocr binary network endpoint. - -The endpoint is not this process's own: rage networking is delegated to the crecore p2p proxy at ---ocr.proxy-address, which hosts the peer --ocr.peer-id names. --pingpong.peers lists every -participant as peerID@host:port, in the order that decides who says hi to whom; the addresses are -where those peers listen, which is how they find each other to begin with. - -Settings can come from flags, from CRE_/CL_ env vars, or from a --config file; -run "docs" to write the full reference to docs/CONFIG.md.`, - } - root.PersistentFlags().String("config", "", "Path to config file") - - opts := flags.DefaultTOMLOptions("CRE", "CL") - opts.Namespace = "pingpong" - if err := flags.RegisterCommandFlags(root, &cfg, opts); err != nil { - return err - } - - bootstrapper := standalone.NewBootstrapper(root) - lggr := bootstrapper.Logger() - - // The proxy dependency, not the host one: this binary drives an endpoint, it does not run a - // peer. Which is also why it needs no database - it is told its peer ID rather than unlocking a - // keystore to find it. - ocrDep := ocr.Proxy(lggr.Named("OCR")) - - return standalone.Run1(bootstrapper, func( - ctx context.Context, - scfg *standalone.StandaloneConfig, - factories *ocr.OCRFactories, - ) []services.Service { - return []services.Service{newPingPong(scfg.Logger.Named("pingpong"), &cfg, factories)} - }, ocrDep) -} - -// Config is what one instance needs to know about the others. Its own place in the ring is not -// configured: it is wherever its peer ID appears in Peers. -type Config struct { - // Each entry is peerID@host:port, because both halves are needed and there is no sense in - // configuring them apart. The peer ID says who an oracle is - its position here is the oracle ID - // the others address it by - and the address is where its peer listens, which libocr needs to - // dial before any of them have heard of each other: a proxy knows only the announcements it has - // seen, and a fresh one has seen none. The address is the crecore --ocr.listen-addresses of the - // process hosting that peer, not its --proxy.listen-address, which is a private arrangement - // between an app and its own proxy. - Peers []string `usage:"peerID@host:port of every participant, in the order that decides who says hi to whom; this instance's own --ocr.peer-id must be one of them" validate:"required" example:"['12D3KooWFirst@127.0.0.1:6690','12D3KooWSecond@127.0.0.1:6691']"` - - StartDelay config.Duration `usage:"how long to wait before the first lap, giving the peers time to connect"` - RoundDelay config.Duration `usage:"how long to wait after a lap completes before starting the next one"` - RetryDelay config.Duration `usage:"how long oracle 0 waits for a lap to come back round before starting it again"` -} - -var defaultConfig = Config{ - StartDelay: *config.MustNewDuration(5 * time.Second), - RoundDelay: *config.MustNewDuration(2 * time.Second), - // Long enough that a lap has really stalled rather than merely being slow, short enough that a - // message dropped before the peers had connected does not end the demo. - RetryDelay: *config.MustNewDuration(5 * time.Second), -} diff --git a/crecore/pingpong/run_4.sh b/crecore/pingpong/run_4.sh deleted file mode 100755 index 52b083043..000000000 --- a/crecore/pingpong/run_4.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# -# Runs four crecore p2p proxies and four pingpong instances against them, on this machine. -# -# Each proxy hosts one peer, so each needs a keystore of its own to take that peer's identity from - -# crecore reads the first key ring in the database it is pointed at. That means four databases: one -# shared one would hand every proxy the same key, and four proxies claiming one peer ID is not a -# network. They are created here, seeded by ./setup, and dropped again on the next run. -# -# Because each proxy then keeps its own record of the announcements it has seen, a fresh one knows -# of nobody. The apps are configured with peerID@address for every peer, which is how each proxy is -# told where the others listen. -# -# Needs a postgres to create databases in. Point DATABASE_URL at one; the default is a local server -# with the usual postgres/postgres credentials, e.g. -# -# docker run --rm -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres --name cre-pingpong-db postgres:16 -# -# Ctrl-C stops everything. -set -euo pipefail - -cd "$(dirname "$0")" - -INSTANCES=${INSTANCES:-4} -DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable} -# One password for every shard: they are four halves of one demo, not four operators. -KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD:-pingpong-keystore-password} - -DB_PREFIX=${DB_PREFIX:-cre_pingpong} -P2P_PORT=${P2P_PORT:-6690} # what each proxy's rage peer listens on -PROXY_PORT=${PROXY_PORT:-50051} # what each proxy serves its gRPC on, and what its app dials -HEALTH_PORT=${HEALTH_PORT:-9100} # /healthz and /metrics, one port per proxy - -# The registry crecore serves alongside the proxy always runs and wants an address and an RPC. There -# is no chain here, so it is pointed at a dead one: it logs that it cannot sync, and the proxy - the -# part this demo uses - is unaffected. -REGISTRY_ADDRESS=${REGISTRY_ADDRESS:-0x0000000000000000000000000000000000000001} -EVM_HTTP_URL=${EVM_HTTP_URL:-http://127.0.0.1:1} -EVM_CHAIN_ID=${EVM_CHAIN_ID:-1} - -BIN=$(mktemp -d) -LOGS=${LOGS:-$(mktemp -d)} -pids=() - -cleanup() { - trap - INT TERM EXIT - echo - echo "stopping..." - for pid in "${pids[@]:-}"; do - [[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true - done - wait 2>/dev/null || true - rm -rf "$BIN" -} -trap cleanup INT TERM EXIT - -# psql_admin runs SQL against the server itself rather than one of the demo databases, for creating -# and dropping them. -psql_admin() { psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -c "$1"; } - -# database_url returns the url of instance $1's database, keeping the credentials and options of -# DATABASE_URL and replacing only the database name. -database_url() { - python3 - "$DATABASE_URL" "${DB_PREFIX}_$1" <<-'EOF' - import sys - from urllib.parse import urlsplit, urlunsplit - url, name = sys.argv[1], sys.argv[2] - parts = urlsplit(url) - print(urlunsplit(parts._replace(path="/" + name))) - EOF -} - -echo "building..." -go build -o "$BIN/crecore" .. -go build -o "$BIN/pingpong" . -go build -o "$BIN/setup" ./setup - -echo "preparing $INSTANCES databases and keystores..." -peers=() -for i in $(seq 0 $((INSTANCES - 1))); do - psql_admin "DROP DATABASE IF EXISTS ${DB_PREFIX}_$i" - psql_admin "CREATE DATABASE ${DB_PREFIX}_$i" - peer_id=$("$BIN/setup" -url "$(database_url "$i")" -password "$KEYSTORE_PASSWORD") - # Where instance i's peer lives: its ID, and the address the proxy hosting it listens on. - peers+=("$peer_id@127.0.0.1:$((P2P_PORT + i))") -done - -# Every peer in one comma-separated list: a peer's position here is the oracle ID the others address -# it by, so instance i is oracle i. -peer_list=$(IFS=,; echo "${peers[*]}") - -echo "starting $INSTANCES proxies..." -for i in $(seq 0 $((INSTANCES - 1))); do - "$BIN/crecore" run \ - --ocr.listen-addresses="127.0.0.1:$((P2P_PORT + i))" \ - --ocr.keystore-password="$KEYSTORE_PASSWORD" \ - --database.url="$(database_url "$i")" \ - --grpc.host="127.0.0.1" \ - --grpc.port="$((PROXY_PORT + i))" \ - --http.port="$((HEALTH_PORT + i))" \ - --capabilities-registry.address="$REGISTRY_ADDRESS" \ - --evm.chain-id="$EVM_CHAIN_ID" \ - --evm.http-url="$EVM_HTTP_URL" \ - >"$LOGS/crecore_$i.log" 2>&1 & - pids+=($!) -done - -# Long enough for each proxy to have opened its database, unlocked its key and bound its port; the -# apps retry anyway, so this only keeps the first few seconds of output tidy. -sleep 5 - -echo "starting $INSTANCES apps..." -for i in $(seq 0 $((INSTANCES - 1))); do - "$BIN/pingpong" run \ - --ocr.proxy-address="127.0.0.1:$((PROXY_PORT + i))" \ - --ocr.peer-id="${peers[$i]%@*}" \ - --http.port=""$((HEALTH_PORT + i + INSTANCES))"" \ - --pingpong.peers="$peer_list" \ - 2>"$LOGS/pingpong_$i.log" & - pids+=($!) -done - -echo -echo "logs are in $LOGS; the messages below are the apps' stdout" -echo -wait diff --git a/crecore/pingpong/service.go b/crecore/pingpong/service.go deleted file mode 100644 index 7e2d6fae7..000000000 --- a/crecore/pingpong/service.go +++ /dev/null @@ -1,225 +0,0 @@ -package main - -import ( - "context" - "crypto/sha256" - "fmt" - "math" - "slices" - "strings" - "time" - - "github.com/smartcontractkit/libocr/commontypes" - ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - - "github.com/smartcontractkit/capabilities/libs/standalone/ocr" -) - -// The two messages this app sends. A lap is a chain of hi, and the broadcast marks where one lap -// ends and the next begins - useful when reading four instances' output interleaved in one terminal. -const ( - messageHi = "hi" - messageStartingAgain = "starting again" -) - -// configDigest identifies the group of oracles talking to each other. Every instance must use the -// same one, and libocr keys an endpoint by it, so it is a constant rather than a setting: there is -// one group here and it is this app's. -var configDigest = ocr2types.ConfigDigest(sha256.Sum256([]byte("cre/pingpong/v1"))) - -// endpointLimits are generous for messages this small; they exist because libocr requires them. -var endpointLimits = ocr2types.BinaryNetworkEndpointLimits{ - MaxMessageLength: 1024, - MessagesRatePerOracle: 10, - MessagesCapacityPerOracle: 100, - BytesRatePerOracle: 10 * 1024, - BytesCapacityPerOracle: 100 * 1024, -} - -// pingPong passes a message round the ring of oracles over one binary network endpoint. -type pingPong struct { - services.Service - eng *services.Engine - - lggr logger.Logger - cfg *Config - factories *ocr.OCRFactories - - // me is this instance's position in cfg.Peers, which is also the oracle ID every other instance - // addresses it by. Resolved in start, since it depends on the peer ID the proxy hosts for us. - me commontypes.OracleID - // oracles is how many of us there are, from cfg.Peers. - oracles int - - endpoint commontypes.BinaryNetworkEndpoint - // lapDone carries the hi that came back round to oracle 0, telling it to start the next lap. - // Buffered so a lap completing while 0 is between waits is not lost. - lapDone chan struct{} -} - -var _ services.Service = (*pingPong)(nil) - -func newPingPong(lggr logger.Logger, cfg *Config, factories *ocr.OCRFactories) *pingPong { - p := &pingPong{lggr: lggr, cfg: cfg, factories: factories, lapDone: make(chan struct{}, 1)} - p.Service, p.eng = services.Config{ - Name: "PingPong", - Start: p.start, - Close: p.close, - }.NewServiceEngine(lggr) - return p -} - -func (p *pingPong) start(ctx context.Context) error { - peerIDs, locators, err := parsePeers(p.cfg.Peers) - if err != nil { - return err - } - // An oracle ID is a uint8, so a ring longer than that cannot address its own members. - if len(peerIDs) < 2 || len(peerIDs) > math.MaxUint8 { - return fmt.Errorf("--pingpong.peers needs between two and %d peers to pass a message between, got %d", math.MaxUint8, len(peerIDs)) - } - p.oracles = len(peerIDs) - - // Which oracle this instance is, is not configured: it is where the peer the proxy hosts for us - // appears in the ring. Being a position in a ring that was just bounded, it fits an oracle ID. - me := slices.Index(peerIDs, p.factories.PeerID.String()) - if me < 0 || me > math.MaxUint8 { - return fmt.Errorf("this instance's peer %s is not one of --pingpong.peers %v", p.factories.PeerID, peerIDs) - } - p.me = commontypes.OracleID(me) - - // Created through the proxy: the peer that dials, announces and encrypts is in the crecore - // process at --ocr.proxy-address, and what comes back here is a plain endpoint. The peers' - // addresses go with it, since that proxy has no other way of knowing where they are. - endpoint, err := p.factories.OCR2Endpoint.NewEndpoint(configDigest, peerIDs, locators, 1, endpointLimits) - if err != nil { - return fmt.Errorf("failed to create endpoint: %w", err) - } - if err := endpoint.Start(); err != nil { - return fmt.Errorf("failed to start endpoint: %w", err) - } - p.endpoint = endpoint - - p.lggr.Infow("Ping pong ready", "oracle", p.me, "peerID", p.factories.PeerID.String(), "oracles", p.oracles) - - p.eng.Go(p.receive) - if p.me == 0 { - p.eng.Go(p.lead) - } - return nil -} - -// receive prints every message that arrives and passes the lap on. -func (p *pingPong) receive(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case msg := <-p.endpoint.Receive(): - fmt.Printf("I am %d and got the message %s from %d\n", p.me, msg.Msg, msg.Sender) - p.forward(string(msg.Msg)) - } - } -} - -// forward continues the lap: everyone but the last says hi to the next oracle along, the last says -// hi back to oracle 0, and oracle 0 - whose hi can only have come from the last - takes that as the -// lap being complete. -func (p *pingPong) forward(message string) { - if message != messageHi { - return // the broadcast marking a new lap; oracle 0 is about to say hi anyway - } - - switch { - case p.me == 0: - select { - case p.lapDone <- struct{}{}: - default: // a lap is already waiting to be counted - } - case int(p.me) == p.oracles-1: - p.send(messageHi, 0) - default: - p.send(messageHi, p.me+1) - } -} - -// lead is oracle 0's job: start a lap, wait for it to come back, announce the next one and go again. -// It re-sends rather than waiting forever, because a hi sent before the peers have finished -// connecting is dropped like any other message to an unreachable peer, and one lost message would -// otherwise leave every instance waiting quietly. -func (p *pingPong) lead(ctx context.Context) { - if !sleep(ctx, p.cfg.StartDelay.Duration()) { - return - } - - retry := time.NewTimer(p.cfg.RetryDelay.Duration()) - defer retry.Stop() - - for { - p.send(messageHi, 1) - retry.Reset(p.cfg.RetryDelay.Duration()) - - select { - case <-ctx.Done(): - return - case <-p.lapDone: - p.lggr.Infow("Lap complete, starting again") - p.broadcast(messageStartingAgain) - if !sleep(ctx, p.cfg.RoundDelay.Duration()) { - return - } - case <-retry.C: - p.lggr.Infow("Lap did not come back round, starting it again") - } - } -} - -func (p *pingPong) send(message string, to commontypes.OracleID) { - p.lggr.Debugw("Sending", "message", message, "to", to) - p.endpoint.SendTo([]byte(message), to) -} - -func (p *pingPong) broadcast(message string) { - p.lggr.Debugw("Broadcasting", "message", message) - p.endpoint.Broadcast([]byte(message)) -} - -func (p *pingPong) close() error { - if p.endpoint != nil { - return p.endpoint.Close() - } - return nil -} - -// parsePeers splits peerID@host:port entries into the two things libocr wants them as: the peer IDs, -// in order, which is the oracle set and the ring; and the same peers as locators, which is how the -// hosting proxies are told where to find each other. -func parsePeers(peers []string) (peerIDs []string, locators []commontypes.BootstrapperLocator, err error) { - peerIDs = make([]string, 0, len(peers)) - locators = make([]commontypes.BootstrapperLocator, 0, len(peers)) - - for _, peer := range peers { - peerID, address, found := strings.Cut(peer, "@") - if !found || peerID == "" || address == "" { - return nil, nil, fmt.Errorf("invalid --pingpong.peers entry %q: expected peerID@host:port", peer) - } - peerIDs = append(peerIDs, peerID) - locators = append(locators, commontypes.BootstrapperLocator{PeerID: peerID, Addrs: []string{address}}) - } - return peerIDs, locators, nil -} - -// sleep waits for d, reporting false if the context was cancelled first. -func sleep(ctx context.Context, d time.Duration) bool { - timer := time.NewTimer(d) - defer timer.Stop() - select { - case <-ctx.Done(): - return false - case <-timer.C: - return true - } -} diff --git a/crecore/pingpong/setup/main.go b/crecore/pingpong/setup/main.go deleted file mode 100644 index 32a6cbbe2..000000000 --- a/crecore/pingpong/setup/main.go +++ /dev/null @@ -1,96 +0,0 @@ -// Command setup gives one crecore proxy a database to run against: a keystore holding a fresh P2P -// key, in the shape crecore expects to find a node's keystore in. -// -// crecore borrows its peer identity from the node whose database it shares, by decrypting the -// node's key ring (the legacy encrypted_key_rings table). There is no node here, so this writes -// that row itself. It prints the peer ID of the key it created and nothing else, so a script can -// capture it and tell the app which peer it is talking as. -// -// For local demos only. The key is generated here and encrypted with a password passed on the -// command line, which is fine for something whose whole purpose is to say hi to itself four times. -package main - -import ( - "context" - "database/sql" - "flag" - "fmt" - "log" - "time" - - "github.com/smartcontractkit/chainlink-common/keystore" - "github.com/smartcontractkit/chainlink-common/keystore/corekeys/models" - "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" - "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" -) - -func main() { - url := flag.String("url", "", "postgres url of the database this proxy will use") - password := flag.String("password", "", "password the key ring is encrypted with; the proxy's --ocr.keystore-password") - flag.Parse() - - if *url == "" || *password == "" { - log.Fatal("both -url and -password are required") - } - - peerID, err := setup(context.Background(), *url, *password) - if err != nil { - log.Fatal(err) - } - - // The only thing on stdout, so `peer_id=$(setup ...)` works. - fmt.Println(peerID) -} - -// setup writes a key ring holding one new P2P key, replacing whatever was there, and returns the -// peer ID of that key. -func setup(ctx context.Context, url, password string) (string, error) { - db, err := sqlutil.OpenDB(sqlutil.Config{URL: url}) - if err != nil { - return "", fmt.Errorf("failed to open database: %w", err) - } - defer func() { _ = db.Close() }() - - key, err := p2pkey.NewV2() - if err != nil { - return "", fmt.Errorf("failed to generate a P2P key: %w", err) - } - - ring := models.NewKeyRing() - ring.P2P[key.ID()] = key - // Fast parameters: this is a throwaway key on a local machine, and the default ones take long - // enough per instance to be irritating. - encrypted, err := ring.Encrypt(password, keystore.FastScryptParams) - if err != nil { - return "", fmt.Errorf("failed to encrypt the key ring: %w", err) - } - - if err := writeKeyRing(ctx, db, encrypted.EncryptedKeys); err != nil { - return "", err - } - return key.ID(), nil -} - -// writeKeyRing creates the keystore table if this database has never held one - a node would have -// migrated it in - and leaves exactly one key ring in it. -func writeKeyRing(ctx context.Context, db *sql.DB, encryptedKeys []byte) error { - const createTable = `CREATE TABLE IF NOT EXISTS encrypted_key_rings ( - encrypted_keys bytea NOT NULL, - updated_at timestamptz NOT NULL - )` - if _, err := db.ExecContext(ctx, createTable); err != nil { - return fmt.Errorf("failed to create encrypted_key_rings: %w", err) - } - - // Replaced rather than added to: crecore takes the first row it finds, so a second one would - // make which peer it is a matter of luck. - if _, err := db.ExecContext(ctx, `DELETE FROM encrypted_key_rings`); err != nil { - return fmt.Errorf("failed to clear encrypted_key_rings: %w", err) - } - if _, err := db.ExecContext(ctx, - `INSERT INTO encrypted_key_rings (encrypted_keys, updated_at) VALUES ($1, $2)`, - encryptedKeys, time.Now()); err != nil { - return fmt.Errorf("failed to write the key ring: %w", err) - } - return nil -} diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go index 78eb4fd2f..e9392d024 100644 --- a/crecore/proxy_service.go +++ b/crecore/proxy_service.go @@ -6,7 +6,7 @@ import ( "google.golang.org/grpc" - "github.com/smartcontractkit/capabilities/libs/standalone/ocr" + "github.com/smartcontractkit/capabilities/libs/standalone/rage" "github.com/smartcontractkit/capabilities/libs/x/registry" "github.com/smartcontractkit/chainlink-common/pkg/config" @@ -45,7 +45,7 @@ type proxyService struct { // CapabilitiesRegistry) has registered too, so they share one address instead of each opening a // listener of their own. grpcServer grpc.ServiceRegistrar - factories *ocr.RageFactories + factories *rage.Factories } var _ services.Service = (*proxyService)(nil) @@ -53,7 +53,7 @@ var _ services.Service = (*proxyService)(nil) // newProxyService builds the proxy service using the standard // services.Config/Engine pattern, so its lifecycle and health integrate with // the bootstrapper's aggregated health report. -func newProxyService(lggr logger.Logger, grpcServer grpc.ServiceRegistrar, factories *ocr.RageFactories) *proxyService { +func newProxyService(lggr logger.Logger, grpcServer grpc.ServiceRegistrar, factories *rage.Factories) *proxyService { s := &proxyService{lggr: lggr, grpcServer: grpcServer, factories: factories} s.Service, s.eng = services.Config{ Name: "P2PProxy", diff --git a/libs/go.sum b/libs/go.sum index d429e25ba..b2af0106e 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -628,12 +628,8 @@ github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97M github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= -github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyipsk1EYnW3riZyjDVKmhz3769JpnHU= -github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= diff --git a/libs/standalone/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go index 300c0f68a..3f666b424 100644 --- a/libs/standalone/bootstrap_gen.go +++ b/libs/standalone/bootstrap_gen.go @@ -28,9 +28,9 @@ func Run1[T0 any]( fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0) []services.Service, bootDep0 common.BootstrapDependency[T0], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -43,9 +43,9 @@ func Run1[T0 any]( }, []common.BootstrapCommand{bootDep0}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1)}, ) } @@ -68,11 +68,11 @@ func Run2[T0 any, T1 any]( bootDep0 common.BootstrapDependency[T0], bootDep1 common.BootstrapDependency[T1], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -90,9 +90,9 @@ func Run2[T0 any, T1 any]( }, []common.BootstrapCommand{bootDep0, bootDep1}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1)}, ) } @@ -116,13 +116,13 @@ func Run3[T0 any, T1 any, T2 any]( bootDep1 common.BootstrapDependency[T1], bootDep2 common.BootstrapDependency[T2], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -145,9 +145,9 @@ func Run3[T0 any, T1 any, T2 any]( }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1)}, ) } @@ -172,15 +172,15 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( bootDep2 common.BootstrapDependency[T2], bootDep3 common.BootstrapDependency[T3], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) - dep3 := instanceOf(bs, bootDep3, index, embed) + dep3 := instanceOf(bs, bootDep3, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -208,9 +208,9 @@ func Run4[T0 any, T1 any, T2 any, T3 any]( }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1), bootDep3.ForEmbedding(0, 1)}, ) } @@ -236,17 +236,17 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( bootDep3 common.BootstrapDependency[T3], bootDep4 common.BootstrapDependency[T4], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) - dep3 := instanceOf(bs, bootDep3, index, embed) + dep3 := instanceOf(bs, bootDep3, index, count, embed) - dep4 := instanceOf(bs, bootDep4, index, embed) + dep4 := instanceOf(bs, bootDep4, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -279,9 +279,9 @@ func Run5[T0 any, T1 any, T2 any, T3 any, T4 any]( }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1), bootDep3.ForEmbedding(0, 1), bootDep4.ForEmbedding(0, 1)}, ) } @@ -308,19 +308,19 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( bootDep4 common.BootstrapDependency[T4], bootDep5 common.BootstrapDependency[T5], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) - dep3 := instanceOf(bs, bootDep3, index, embed) + dep3 := instanceOf(bs, bootDep3, index, count, embed) - dep4 := instanceOf(bs, bootDep4, index, embed) + dep4 := instanceOf(bs, bootDep4, index, count, embed) - dep5 := instanceOf(bs, bootDep5, index, embed) + dep5 := instanceOf(bs, bootDep5, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -358,9 +358,9 @@ func Run6[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any]( }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1), bootDep3.ForEmbedding(0, 1), bootDep4.ForEmbedding(0, 1), bootDep5.ForEmbedding(0, 1)}, ) } @@ -388,21 +388,21 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( bootDep5 common.BootstrapDependency[T5], bootDep6 common.BootstrapDependency[T6], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) - dep3 := instanceOf(bs, bootDep3, index, embed) + dep3 := instanceOf(bs, bootDep3, index, count, embed) - dep4 := instanceOf(bs, bootDep4, index, embed) + dep4 := instanceOf(bs, bootDep4, index, count, embed) - dep5 := instanceOf(bs, bootDep5, index, embed) + dep5 := instanceOf(bs, bootDep5, index, count, embed) - dep6 := instanceOf(bs, bootDep6, index, embed) + dep6 := instanceOf(bs, bootDep6, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -445,9 +445,9 @@ func Run7[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any]( }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1), bootDep3.ForEmbedding(0, 1), bootDep4.ForEmbedding(0, 1), bootDep5.ForEmbedding(0, 1), bootDep6.ForEmbedding(0, 1)}, ) } @@ -476,23 +476,23 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( bootDep6 common.BootstrapDependency[T6], bootDep7 common.BootstrapDependency[T7], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) - dep3 := instanceOf(bs, bootDep3, index, embed) + dep3 := instanceOf(bs, bootDep3, index, count, embed) - dep4 := instanceOf(bs, bootDep4, index, embed) + dep4 := instanceOf(bs, bootDep4, index, count, embed) - dep5 := instanceOf(bs, bootDep5, index, embed) + dep5 := instanceOf(bs, bootDep5, index, count, embed) - dep6 := instanceOf(bs, bootDep6, index, embed) + dep6 := instanceOf(bs, bootDep6, index, count, embed) - dep7 := instanceOf(bs, bootDep7, index, embed) + dep7 := instanceOf(bs, bootDep7, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -540,9 +540,9 @@ func Run8[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any]( }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1), bootDep3.ForEmbedding(0, 1), bootDep4.ForEmbedding(0, 1), bootDep5.ForEmbedding(0, 1), bootDep6.ForEmbedding(0, 1), bootDep7.ForEmbedding(0, 1)}, ) } @@ -572,25 +572,25 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any bootDep7 common.BootstrapDependency[T7], bootDep8 common.BootstrapDependency[T8], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) - dep3 := instanceOf(bs, bootDep3, index, embed) + dep3 := instanceOf(bs, bootDep3, index, count, embed) - dep4 := instanceOf(bs, bootDep4, index, embed) + dep4 := instanceOf(bs, bootDep4, index, count, embed) - dep5 := instanceOf(bs, bootDep5, index, embed) + dep5 := instanceOf(bs, bootDep5, index, count, embed) - dep6 := instanceOf(bs, bootDep6, index, embed) + dep6 := instanceOf(bs, bootDep6, index, count, embed) - dep7 := instanceOf(bs, bootDep7, index, embed) + dep7 := instanceOf(bs, bootDep7, index, count, embed) - dep8 := instanceOf(bs, bootDep8, index, embed) + dep8 := instanceOf(bs, bootDep8, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -643,9 +643,9 @@ func Run9[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1), bootDep3.ForEmbedding(0, 1), bootDep4.ForEmbedding(0, 1), bootDep5.ForEmbedding(0, 1), bootDep6.ForEmbedding(0, 1), bootDep7.ForEmbedding(0, 1), bootDep8.ForEmbedding(0, 1)}, ) } @@ -676,27 +676,27 @@ func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 an bootDep8 common.BootstrapDependency[T8], bootDep9 common.BootstrapDependency[T9], ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { - dep0 := instanceOf(bs, bootDep0, index, embed) + dep0 := instanceOf(bs, bootDep0, index, count, embed) - dep1 := instanceOf(bs, bootDep1, index, embed) + dep1 := instanceOf(bs, bootDep1, index, count, embed) - dep2 := instanceOf(bs, bootDep2, index, embed) + dep2 := instanceOf(bs, bootDep2, index, count, embed) - dep3 := instanceOf(bs, bootDep3, index, embed) + dep3 := instanceOf(bs, bootDep3, index, count, embed) - dep4 := instanceOf(bs, bootDep4, index, embed) + dep4 := instanceOf(bs, bootDep4, index, count, embed) - dep5 := instanceOf(bs, bootDep5, index, embed) + dep5 := instanceOf(bs, bootDep5, index, count, embed) - dep6 := instanceOf(bs, bootDep6, index, embed) + dep6 := instanceOf(bs, bootDep6, index, count, embed) - dep7 := instanceOf(bs, bootDep7, index, embed) + dep7 := instanceOf(bs, bootDep7, index, count, embed) - dep8 := instanceOf(bs, bootDep8, index, embed) + dep8 := instanceOf(bs, bootDep8, index, count, embed) - dep9 := instanceOf(bs, bootDep9, index, embed) + dep9 := instanceOf(bs, bootDep9, index, count, embed) return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { value0, err := dep0.Get(ctx) @@ -754,8 +754,8 @@ func Run10[T0 any, T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 an }, []common.BootstrapCommand{bootDep0, bootDep1, bootDep2, bootDep3, bootDep4, bootDep5, bootDep6, bootDep7, bootDep8, bootDep9}, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{bootDep0.ForEmbedding(0), bootDep1.ForEmbedding(0), bootDep2.ForEmbedding(0), bootDep3.ForEmbedding(0), bootDep4.ForEmbedding(0), bootDep5.ForEmbedding(0), bootDep6.ForEmbedding(0), bootDep7.ForEmbedding(0), bootDep8.ForEmbedding(0), bootDep9.ForEmbedding(0)}, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{bootDep0.ForEmbedding(0, 1), bootDep1.ForEmbedding(0, 1), bootDep2.ForEmbedding(0, 1), bootDep3.ForEmbedding(0, 1), bootDep4.ForEmbedding(0, 1), bootDep5.ForEmbedding(0, 1), bootDep6.ForEmbedding(0, 1), bootDep7.ForEmbedding(0, 1), bootDep8.ForEmbedding(0, 1), bootDep9.ForEmbedding(0, 1)}, ) } diff --git a/libs/standalone/bootstrapper.go b/libs/standalone/bootstrapper.go index 486a66a90..d0fc8ca44 100644 --- a/libs/standalone/bootstrapper.go +++ b/libs/standalone/bootstrapper.go @@ -164,10 +164,11 @@ func (b *Bootstrapper) Logger() logger.SugaredLogger { return b.config.Logger } // has nowhere to report that. type instanceServices func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) -// instantiator returns the factory building instance index's services. The generated Run helpers -// supply it, since only they know the dependencies' types; embed says whether to replace each -// dependency with its embedded form (BootstrapDependency.ForEmbedding) first. -type instantiator func(index int, embed bool) instanceServices +// instantiator returns the factory building instance index's services, out of count instances. The +// generated Run helpers supply it, since only they know the dependencies' types; embed says whether +// to replace each dependency with its embedded form (BootstrapDependency.ForEmbedding) first, which +// is also the only case where count means anything. +type instantiator func(index, count int, embed bool) instanceServices // run wires up the subcommands that start the binary and executes the root command. // @@ -252,7 +253,7 @@ func (b *Bootstrapper) runInstances(ctx context.Context, stop context.CancelFunc } for i := range count { - if err := b.startInstance(ctx, i, count, instantiate(i, embed)); err != nil { + if err := b.startInstance(ctx, i, count, instantiate(i, count, embed)); err != nil { stop() return fmt.Errorf("failed to start instance %d: %w", i, err) } @@ -529,15 +530,15 @@ func (d *dependency[T]) Get(ctx context.Context) (T, error) { return v, err } -// instanceOf resolves which dependency instance index uses, and wraps it for that instance's -// factory. Called by the generated Run helpers, once per dependency per instance. +// instanceOf resolves which dependency instance index of count uses, and wraps it for that +// instance's factory. Called by the generated Run helpers, once per dependency per instance. // // A single instance keeps the dependency as it was built rather than embedding it at index 0: // `run` is what the configuration describes literally, and a dependency that partitions itself per // instance should not quietly move a single run's state somewhere else. -func instanceOf[T any](bs *Bootstrapper, bd contract.BootstrapDependency[T], index int, embed bool) *dependency[T] { +func instanceOf[T any](bs *Bootstrapper, bd contract.BootstrapDependency[T], index, count int, embed bool) *dependency[T] { if embed { - bd = bd.ForEmbedding(index) + bd = bd.ForEmbedding(index, count) } return &dependency[T]{bs: bs, bd: bd} } diff --git a/libs/standalone/capability/dependency.go b/libs/standalone/capability/dependency.go index b879a9c88..7ac9b14f2 100644 --- a/libs/standalone/capability/dependency.go +++ b/libs/standalone/capability/dependency.go @@ -15,7 +15,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" - registryclient "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" @@ -30,6 +29,14 @@ type Dependencies struct { CRESettings core.SettingsBroadcaster CapabilityRegistry core.CapabilitiesRegistry + // OCRConfigRegistry is where an OCR-based capability reads the configuration it runs under. + // + // It is a field of its own rather than CapabilityRegistry seen from another side because a + // capability that runs no oracle should not have to know the question exists. Where it comes from + // is the registry either way: the node's for a configured run, and for an embedded one a + // configuration computed over the run's own instances - see RegisterEmbeddedOCRConfig. + OCRConfigRegistry core.OCRConfigRegistry + // CapabilityDonID is the on-chain DON ID of the capability DON this plugin // process was spawned for, resolved authoritatively by the host before // Initialise is called. Plugins should use this as the source of truth for @@ -51,10 +58,10 @@ type Dependencies struct { settings *loop.AtomicSettings // settingsPath is the file the reload endpoint re-reads. settingsPath string - // proxy is the node's registry, which capabilities hosted here are announced - // to by address. nil when embedded: there is no node to announce to, and the - // capabilities are resolved in-process as values. - proxy core.AddressableRegistryBase + // addresses is where the capabilities this binary hosts are served, by capability ID. The + // registry announces a capability at the address it finds here, so the registrar fills an entry + // in as it opens each server - see registrar.serve. Empty when embedded, which announces nothing. + addresses map[string]string // servers opens one gRPC server per capability. nil when embedded, which // serves nothing. servers *standalonegrpc.Factory @@ -62,21 +69,6 @@ type Dependencies struct { closers []func() error } -// OCRConfigRegistry is where an OCR-based capability reads the configuration it -// runs under: the node's registry, which computes the config digest from the -// contract it read - something a capability is deliberately not told. -// -// Nil when this process has no node behind it, as an embedded run does not: an -// oracle then has no configuration to join, which is a clearer failure than one -// invented locally that no other member of the DON agrees with. -func (d Dependencies) OCRConfigRegistry() core.OCRConfigRegistry { - registry, ok := d.proxy.(core.OCRConfigRegistry) - if !ok { - return nil - } - return registry -} - // Close releases whatever Get dialled. The bootstrapper closes resolved // dependency values on shutdown, after the services built from them. func (d Dependencies) Close() error { @@ -116,8 +108,9 @@ func Dependency(lggr logger.Logger, servers common.BootstrapDependency[*standalo // Wrapped so the connections Get dials are made at most once however many // services resolve this. return common.OnceBootstrapper[Dependencies](&dependency{ - lggr: lggr, - servers: servers, + lggr: lggr, + servers: servers, + embeddedConfig: &embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}, }) } @@ -125,6 +118,15 @@ type dependency struct { lggr logger.Logger servers common.BootstrapDependency[*standalonegrpc.Factory] capabilityConfig + + // embeddedConfig is the settings of every embedded form this produces, allocated here so that + // all of them share it. + // + // Sharing is what makes those settings arrive at all: the form whose settings are registered on + // the embed command is the one built to be asked for them, and the forms that go on to resolve + // each instance are built later (see ForEmbedding). A config per form would leave the decoded + // values in the first one and every instance reading the defaults. + embeddedConfig *embeddedConfig } var _ common.BootstrapDependency[Dependencies] = (*dependency)(nil) @@ -141,13 +143,21 @@ func (d *dependency) Dependencies() []common.BootstrapCommand { // has no node to ask, so its registry holds only what this binary registers. See // embedded. // +// Every instance's form reads the one embeddedConfig this dependency holds, so all of them see the +// values the flags were bound to rather than a copy of the defaults. +// // The gRPC factory is embedded rather than passed through, so what an embedded // capability serves on is whatever the factory says instance i gets. Whether that // is one factory for the process or one each is the factory's to decide, and // deciding it here would be this dependency asserting something about another's // internals. -func (d *dependency) ForEmbedding(i int) common.BootstrapDependency[Dependencies] { - return &embedded{lggr: d.lggr, servers: d.servers.ForEmbedding(i), cfg: &embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}} +func (d *dependency) ForEmbedding(i, instances int) common.BootstrapDependency[Dependencies] { + return &embedded{ + lggr: d.lggr, + servers: d.servers.ForEmbedding(i, instances), + instances: instances, + cfg: d.embeddedConfig, + } } func (d *dependency) Get(ctx context.Context, cc common.CommonConfig) (Dependencies, error) { @@ -165,7 +175,11 @@ func (d *dependency) Get(ctx context.Context, cc common.CommonConfig) (Dependenc if err != nil { return Dependencies{}, fmt.Errorf("failed to create registry proxy client for %s: %w", d.ProxyURL, err) } - proxy := registryclient.New(d.lggr, conn) + + // One registry, two questions: what capabilities there are, and what configuration an OCR one + // runs under. Both are answered by whoever read the registry, over the one connection to it. + addresses := map[string]string{} + proxy := registry.Local(d.lggr).WithRemote(conn, addresses) servers, err := d.servers.Get(ctx, cc) if err != nil { @@ -175,12 +189,13 @@ func (d *dependency) Get(ctx context.Context, cc common.CommonConfig) (Dependenc return Dependencies{ LimitsFactory: newLimitsFactory(d.lggr, settings), CRESettings: settings, - CapabilityRegistry: newOverlayRegistry(d.lggr, registry.NewBaseRegistry(d.lggr), proxy), + CapabilityRegistry: proxy, + OCRConfigRegistry: proxy, CapabilityDonID: d.CapabilityDonID, lggr: d.lggr, settings: settings, settingsPath: SettingsPath(), - proxy: proxy, + addresses: addresses, servers: servers, closers: []func() error{proxy.Close, conn.Close}, }, nil @@ -309,53 +324,51 @@ func (r *registrar) start(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to read capability %d info: %w", i, err) } - if err := r.deps.CapabilityRegistry.Add(ctx, c); err != nil { - return fmt.Errorf("failed to register capability %s: %w", info.ID, err) - } - r.hosted = append(r.hosted, hosted{id: info.ID}) - + // Served first, registered second. Registering is what invites traffic - it holds the value + // here and, when there is a registry behind this one, announces the address it is served at - + // so nothing is registered until something can answer. if err := r.serve(ctx, c, info); err != nil { return err } + if err := r.deps.CapabilityRegistry.Add(ctx, c); err != nil { + return fmt.Errorf("failed to register capability %s: %w", info.ID, err) + } r.eng.Infow("Registered capability", "capabilityID", info.ID, "type", info.CapabilityType) } return nil } -// serve binds c to a gRPC server of its own and, when there is a node to tell, -// announces that server's address to its registry. +// serve binds c to a gRPC server of its own and records where that server is, so that registering it +// announces the right address. // -// The server is opened either way. Serving is what makes the capability callable -// from outside this process, and that is worth having under `embed` too - an -// embedded instance's capabilities are reachable in-process as values, but the -// address is what anything else has to go through. Only the announcement needs a -// node, so only the announcement is conditional. +// The server is opened either way. Serving is what makes the capability callable from outside this +// process, and that is worth having under `embed` too - an embedded instance's capabilities are +// reachable in-process as values, but the address is what anything else has to go through. Whether +// the address is announced anywhere is the registry's business: an embedded run has nothing to +// announce to, and its map goes unread. func (r *registrar) serve(ctx context.Context, c Capability, info capabilities.CapabilityInfo) error { server, err := r.deps.servers.New(ctx, logger.Named(r.eng, info.ID)) if err != nil { return fmt.Errorf("failed to open a server for capability %s: %w", info.ID, err) } - r.hosted[len(r.hosted)-1].server = server + r.hosted = append(r.hosted, hosted{id: info.ID, server: server}) - if err := registryclient.RegisterCapability(r.eng, server.Registrar(), c, info.CapabilityType); err != nil { + if err := registry.RegisterCapability(r.eng, server.Registrar(), c, info.CapabilityType); err != nil { return fmt.Errorf("failed to serve capability %s: %w", info.ID, err) } if err := server.Start(ctx); err != nil { return fmt.Errorf("failed to start the server for capability %s: %w", info.ID, err) } - if r.deps.proxy == nil { - return nil - } - if err := r.deps.proxy.AddAt(ctx, info.ID, info.CapabilityType, server.Address()); err != nil { - return fmt.Errorf("failed to announce capability %s at %s: %w", info.ID, server.Address(), err) + if r.deps.addresses != nil { + r.deps.addresses[info.ID] = server.Address() } return nil } // close undoes what start did, in reverse: stop inviting traffic (the registry -// entry, local and announced, which overlayRegistry.Remove drops from both), then -// stop answering it (the server). +// entry, local and announced, which Remove drops from both), then stop answering +// it (the server). // // Failures are logged rather than returned. The process is going away, and a // stale entry in a registry that cannot reach it any more is not worth failing diff --git a/libs/standalone/capability/embed.go b/libs/standalone/capability/embed.go index 0163ac382..31792e030 100644 --- a/libs/standalone/capability/embed.go +++ b/libs/standalone/capability/embed.go @@ -40,6 +40,10 @@ type embedded struct { lggr logger.Logger servers common.BootstrapDependency[*standalonegrpc.Factory] + // instances is how many the run has, which is the DON an OCR-based capability here runs in. + // Nothing else about embedding needs it, and no instance could work it out for itself. + instances int + // cfg is shared with the configured form that produced this one, and with // every other instance's form, so all of them read the settings the flags were // bound to rather than a copy of the defaults. @@ -64,11 +68,16 @@ func (d *embedded) Dependencies() []common.BootstrapCommand { // The gRPC factory is embedded in turn rather than reused as-is: how it serves // instance i is its own business, and this only has to ask it the same question // the configured form did. -func (d *embedded) ForEmbedding(i int) common.BootstrapDependency[Dependencies] { - return &embedded{lggr: d.lggr, servers: d.servers.ForEmbedding(i), cfg: d.cfg} +func (d *embedded) ForEmbedding(i, instances int) common.BootstrapDependency[Dependencies] { + return &embedded{lggr: d.lggr, servers: d.servers.ForEmbedding(i, instances), instances: instances, cfg: d.cfg} } func (d *embedded) Get(ctx context.Context, cc common.CommonConfig) (Dependencies, error) { + // Read once into a copy: the settings are shared with every other instance's form, and what an + // instance resolves itself from should not change halfway through - nor be something it could + // write back to its siblings. + cfg := *d.cfg + settings, err := newSettings(d.lggr) if err != nil { return Dependencies{}, err @@ -82,7 +91,7 @@ func (d *embedded) Get(ctx context.Context, cc common.CommonConfig) (Dependencie return Dependencies{}, fmt.Errorf("failed to get gRPC server factory: %w", err) } - d.lggr.Infow("Using in-process capability registry", "donID", d.cfg.CapabilityDonID) + d.lggr.Infow("Using in-process capability registry", "donID", cfg.CapabilityDonID, "instances", d.instances) // No proxy: only what this binary registers is resolvable, and the registry // metadata calls say so rather than inventing a DON. The capabilities are @@ -90,11 +99,14 @@ func (d *embedded) Get(ctx context.Context, cc common.CommonConfig) (Dependencie return Dependencies{ LimitsFactory: newLimitsFactory(d.lggr, settings), CRESettings: settings, - CapabilityRegistry: newOverlayRegistry(d.lggr, registry.NewBaseRegistry(d.lggr), nil), - CapabilityDonID: d.cfg.CapabilityDonID, - lggr: d.lggr, - settings: settings, - settingsPath: SettingsPath(), - servers: servers, + CapabilityRegistry: registry.Local(d.lggr), + // Computed from the run rather than read from a node, and arriving on the same field it would + // have arrived on either way - see RegisterEmbeddedOCRConfig. + OCRConfigRegistry: embeddedOCRConfigRegistry(d.instances), + CapabilityDonID: cfg.CapabilityDonID, + lggr: d.lggr, + settings: settings, + settingsPath: SettingsPath(), + servers: servers, }, nil } diff --git a/libs/standalone/capability/embed_test.go b/libs/standalone/capability/embed_test.go new file mode 100644 index 000000000..9af3b79c4 --- /dev/null +++ b/libs/standalone/capability/embed_test.go @@ -0,0 +1,56 @@ +package capability + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" + + common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// TestEmbeddedSettingsReachEveryInstance is the wiring that decides whether an embedded run is +// configurable at all: the form the embed command binds its flags to is asked for them before any +// instance exists, and the forms that resolve the instances are built later. They have to be reading +// the same settings, or what was typed stays in the first one. +func TestEmbeddedSettingsReachEveryInstance(t *testing.T) { + d := &dependency{ + lggr: logger.Test(t), + servers: standalonegrpc.FactoryDependency(logger.Test(t)), + embeddedConfig: &embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}, + } + + // What the embed command binds to, before it knows how many instances there will be. + bound, ok := d.ForEmbedding(0, 3).Config().(*embeddedConfig) + require.True(t, ok) + + // Decoded once the command runs, into that same struct. + bound.CapabilityDonID = 7 + + for i := range 3 { + cfg, ok := d.ForEmbedding(i, 3).Config().(*embeddedConfig) + require.True(t, ok, "instance %d", i) + assert.Equal(t, uint32(7), cfg.CapabilityDonID, "instance %d", i) + } +} + +func TestEmbeddedDependencies(t *testing.T) { + d := &dependency{ + lggr: logger.Test(t), + servers: standalonegrpc.FactoryDependency(logger.Test(t)), + embeddedConfig: &embeddedConfig{CapabilityDonID: 9}, + } + + deps, err := d.ForEmbedding(2, 4).Get(t.Context(), common.CommonConfig{}) + require.NoError(t, err) + + assert.Equal(t, uint32(9), deps.CapabilityDonID) + // The registry holds what this binary registers and nothing else: there is no node behind an + // embedded run to ask for the rest. + require.NotNil(t, deps.CapabilityRegistry) + _, err = deps.CapabilityRegistry.Get(t.Context(), "nothing@1.0.0") + require.Error(t, err) +} diff --git a/libs/standalone/capability/ocrconfig.go b/libs/standalone/capability/ocrconfig.go new file mode 100644 index 000000000..83913f8b7 --- /dev/null +++ b/libs/standalone/capability/ocrconfig.go @@ -0,0 +1,77 @@ +package capability + +import ( + "context" + "errors" + "sync" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// EmbeddedOCRConfig builds the registry an embedded run's oracles read their configuration from, +// given how many instances that run has. +type EmbeddedOCRConfig func(oracles int) core.OCRConfigRegistry + +var embeddedOCRConfig struct { + sync.Mutex + build EmbeddedOCRConfig +} + +// RegisterEmbeddedOCRConfig says where an embedded run's OCR configuration comes from. It is called +// from libs/standalone/ocr's init, and by nothing else. +// +// Why it exists at all: a configured capability reads its OCR configuration from the node's +// registry, which this dependency already talks to. An embedded one has no node, so the +// configuration has to be computed from the run itself - the instances, as their derived identities. +// Both have to arrive as the same interface on the same field, or an application could tell which +// kind of run it is, and not being able to tell is the whole point of ForEmbedding. +// +// Why a global rather than an argument: computing that configuration needs derived OCR key bundles, +// and those come from chainlink-common/keystore - one package with a keyring per chain family, so +// reaching for it drags go-ethereum, TON and starknet in behind it. Asking for it here would put all +// of that in the module graph of every capability binary, including ones that host a trigger and run +// no oracle at all. Registering from ocr's init instead means a binary pays for it exactly when it +// links the package that needs it - which an OCR-based capability already does, and a cron trigger +// never will. It is the bargain a database driver strikes with database/sql. +// +// Registering twice panics: two providers disagreeing about a DON is not something to resolve by +// letting the last one win. +// +// Call from an init function. +func RegisterEmbeddedOCRConfig(build EmbeddedOCRConfig) { + embeddedOCRConfig.Lock() + defer embeddedOCRConfig.Unlock() + + if embeddedOCRConfig.build != nil { + panic("capability: an embedded OCR config registry is already registered") + } + embeddedOCRConfig.build = build +} + +// embeddedOCRConfigRegistry is what an embedded run's capabilities read their OCR configuration +// from: the registered builder over this run's instances, or something that says why it cannot +// answer. +func embeddedOCRConfigRegistry(oracles int) core.OCRConfigRegistry { + embeddedOCRConfig.Lock() + build := embeddedOCRConfig.build + embeddedOCRConfig.Unlock() + + if build == nil { + return unregisteredOCRConfig{} + } + return build(oracles) +} + +// unregisteredOCRConfig stands in when nothing registered a builder, which is the ordinary case for a +// capability that runs no oracle: it is handed a registry like any other, and only asking it a +// question it was never going to be able to answer fails. +type unregisteredOCRConfig struct{} + +var _ core.OCRConfigRegistry = unregisteredOCRConfig{} + +func (unregisteredOCRConfig) OCRConfig(context.Context, string, uint32, string) (ocrtypes.ContractConfig, error) { + return ocrtypes.ContractConfig{}, errors.New( + "this embedded run computes no OCR configuration: nothing registered one, which is what linking libs/standalone/ocr does") +} diff --git a/libs/standalone/db/db_dependency.go b/libs/standalone/db/db_dependency.go index 6a2e4272b..595441a05 100644 --- a/libs/standalone/db/db_dependency.go +++ b/libs/standalone/db/db_dependency.go @@ -56,7 +56,7 @@ func (d *dependency) Dependencies() []standalone.BootstrapCommand { // schema: a run of N instances is N nodes, and having one of them live somewhere else would make its // state the odd one out in exactly the situation the schemas exist to keep tidy. A plain `run` never // calls this, so it is untouched by any of it. -func (d *dependency) ForEmbedding(i int) standalone.BootstrapDependency[*sql.DB] { +func (d *dependency) ForEmbedding(i, _ int) standalone.BootstrapDependency[*sql.DB] { return &embedded{dependency: d, schema: fmt.Sprintf("node_%d", i)} } diff --git a/libs/standalone/db/db_dependency_test.go b/libs/standalone/db/db_dependency_test.go index eaa72c818..a82bcdfcf 100644 --- a/libs/standalone/db/db_dependency_test.go +++ b/libs/standalone/db/db_dependency_test.go @@ -46,8 +46,8 @@ func TestForEmbedding(t *testing.T) { // Instance 0 is partitioned like every other instance, so a run of N instances has N schemas and // none of them is the odd one out. A single instance never calls this, and keeps the database as // configured. - assert.Equal(t, "node_0", template.ForEmbedding(0).(*embedded).schema) - assert.Equal(t, "node_1", template.ForEmbedding(1).(*embedded).schema) + assert.Equal(t, "node_0", template.ForEmbedding(0, 2).(*embedded).schema) + assert.Equal(t, "node_1", template.ForEmbedding(1, 2).(*embedded).schema) } func mustParse(t *testing.T, rawURL string) *url.URL { diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl index 0e534e41b..53f7d04c5 100644 --- a/libs/standalone/gen/bootstrap.go.tmpl +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -30,9 +30,9 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( bootDep{{.}} common.BootstrapDependency[T{{.}}], {{- end}} ) error { - return bs.run(func(index int, embed bool) instanceServices { + return bs.run(func(index, count int, embed bool) instanceServices { {{range RangeNum .}} - dep{{.}} := instanceOf(bs, bootDep{{.}}, index, embed) + dep{{.}} := instanceOf(bs, bootDep{{.}}, index, count, embed) {{end}} return func(ctx context.Context, cfg *StandaloneConfig) ([]services.Service, error) { {{- range RangeNum .}} @@ -46,9 +46,9 @@ func Run{{.}}[{{range RangeNum .}}T{{.}} any, {{end}}]( }, []common.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}, {{end}} }, // The embedded form of each dependency, so its settings are registered on the command that - // resolves it. Instance 0's, since a form is asked for its settings and not for its index; - // each instance builds its own when it starts. - []common.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}.ForEmbedding(0), {{end}} }, + // resolves it. Instance 0 of a run of one, since a form is asked for its settings and not for + // its place in the run; each instance builds its own, knowing both, when it starts. + []common.BootstrapCommand{ {{range RangeNum .}}bootDep{{.}}.ForEmbedding(0, 1), {{end}} }, ) } {{- end }} diff --git a/libs/standalone/grpc/dependency.go b/libs/standalone/grpc/dependency.go index 6d8ec6ae6..9ea5ab5ec 100644 --- a/libs/standalone/grpc/dependency.go +++ b/libs/standalone/grpc/dependency.go @@ -40,12 +40,22 @@ type Config struct { // does: a binary running one server calls it what that server is for. func Dependency(lggr logger.Logger) standalone.BootstrapDependency[*Server] { // Wrapped so the port is bound once however many services resolve this. - return standalone.OnceBootstrapper[*Server](&dependency{lggr: lggr, cfg: Config{Host: defaultHost}}) + return standalone.OnceBootstrapper[*Server](&dependency{lggr: lggr, cfg: &Config{Host: defaultHost}}) } type dependency struct { - lggr logger.Logger - cfg Config + lggr logger.Logger + + // cfg is the settings every form of this dependency reads, held by pointer so that the form the + // flags are bound to and the forms that resolve each instance are the same settings. + // + // A copy per form would not do: a form is asked for its settings before the command runs and + // before any instance exists, and the forms that go on to serve the instances are built after + // the values have been decoded - into whichever form was asked. Every instance would then read + // the defaults. See ForEmbedding. + cfg *Config + + // index is which instance this serves, and the only thing that differs between the forms above. index int } @@ -53,21 +63,25 @@ var _ standalone.BootstrapDependency[*Server] = (*dependency)(nil) func (d *dependency) Namespace() string { return namespace } -func (d *dependency) Config() any { return &d.cfg } +func (d *dependency) Config() any { return d.cfg } func (d *dependency) Dependencies() []standalone.BootstrapCommand { return nil } // ForEmbedding gives instance i the configured port plus i, so instances sharing // a process do not collide over it - the same rule the metrics/health server // follows. The settings are otherwise the configured ones: an address is an -// address whether or not there are siblings. -func (d *dependency) ForEmbedding(i int) standalone.BootstrapDependency[*Server] { +// address whether or not there are siblings, which is why they are the same +// settings rather than a copy of them. +func (d *dependency) ForEmbedding(i, _ int) standalone.BootstrapDependency[*Server] { return standalone.OnceBootstrapper[*Server](&dependency{lggr: d.lggr, cfg: d.cfg, index: i}) } func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (*Server, error) { - port := d.cfg.Port + uint16(d.index) - return newServer(ctx, d.lggr, net.JoinHostPort(d.cfg.Host, strconv.Itoa(int(port)))) + // Read once into a copy, so what this server is opened on cannot change under it while it is + // being opened - and so nothing here can write back to settings its siblings are reading. + cfg := *d.cfg + port := cfg.Port + uint16(d.index) + return newServer(ctx, d.lggr, net.JoinHostPort(cfg.Host, strconv.Itoa(int(port)))) } // FactoryConfig configures the servers the factory makes: where they are @@ -152,7 +166,9 @@ func (d *factoryDependency) Dependencies() []standalone.BootstrapCommand { retur // Partitioning them per instance the way the configured server does would need a // stride - how many servers an instance opens - that only the instance knows, and // a shared counter needs no such guess. -func (d *factoryDependency) ForEmbedding(_ int) standalone.BootstrapDependency[*Factory] { return d } +func (d *factoryDependency) ForEmbedding(_, _ int) standalone.BootstrapDependency[*Factory] { + return d +} func (d *factoryDependency) Get(_ context.Context, _ standalone.CommonConfig) (*Factory, error) { return &Factory{host: d.cfg.AdvertiseHost, startPort: d.cfg.StartPort}, nil diff --git a/libs/standalone/grpc/dependency_test.go b/libs/standalone/grpc/dependency_test.go index bc65f01bf..8ba2315cb 100644 --- a/libs/standalone/grpc/dependency_test.go +++ b/libs/standalone/grpc/dependency_test.go @@ -12,7 +12,7 @@ import ( ) func TestDependency_BindsConfiguredPort(t *testing.T) { - d := &dependency{lggr: logger.Test(t), cfg: Config{Host: defaultHost, Port: freePort(t)}} + d := &dependency{lggr: logger.Test(t), cfg: &Config{Host: defaultHost, Port: freePort(t)}} srv, err := d.Get(t.Context(), standalone.CommonConfig{}) require.NoError(t, err) @@ -26,15 +26,36 @@ func TestDependency_BindsConfiguredPort(t *testing.T) { // instances sharing a process do not collide. func TestDependency_ForEmbeddingOffsetsPort(t *testing.T) { base := freePort(t) - d := &dependency{lggr: logger.Test(t), cfg: Config{Host: defaultHost, Port: base}} + d := &dependency{lggr: logger.Test(t), cfg: &Config{Host: defaultHost, Port: base}} - srv, err := d.ForEmbedding(2).Get(t.Context(), standalone.CommonConfig{}) + srv, err := d.ForEmbedding(2, 3).Get(t.Context(), standalone.CommonConfig{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, srv.Close()) }) assert.Equal(t, base+2, uint16Port(t, srv)) } +// The settings an embed run is given are decoded into the form that was asked for them, which is +// the one ForEmbedding built first - so the forms that go on to resolve the instances have to be +// reading that same struct. A copy each, and every instance binds the default port instead. +func TestDependency_EmbeddedInstancesReadTheConfiguredSettings(t *testing.T) { + base := freePort(t) + // Built the way Dependency builds it, before anything has been decoded. + dep := standalone.OnceBootstrapper[*Server](&dependency{lggr: logger.Test(t), cfg: &Config{Host: defaultHost}}) + + // What the embed command binds its flags to, asked for before any instance exists. + bound, ok := dep.ForEmbedding(0, 2).Config().(*Config) + require.True(t, ok) + bound.Port = base + + // Decoded by then, so this is the port instance 1 has to bind. + srv, err := dep.ForEmbedding(1, 2).Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, srv.Close()) }) + + assert.Equal(t, base+1, uint16Port(t, srv)) +} + func TestFactory_BindsEphemeralPortAndReportsIt(t *testing.T) { fd := &factoryDependency{lggr: logger.Test(t), cfg: FactoryConfig{AdvertiseHost: defaultHost}} f, err := fd.Get(t.Context(), standalone.CommonConfig{}) @@ -80,9 +101,9 @@ func TestFactory_SharedAcrossEmbeddedInstances(t *testing.T) { cfg: FactoryConfig{AdvertiseHost: defaultHost, StartPort: start}, }) - first, err := dep.ForEmbedding(0).Get(t.Context(), standalone.CommonConfig{}) + first, err := dep.ForEmbedding(0, 2).Get(t.Context(), standalone.CommonConfig{}) require.NoError(t, err) - second, err := dep.ForEmbedding(1).Get(t.Context(), standalone.CommonConfig{}) + second, err := dep.ForEmbedding(1, 2).Get(t.Context(), standalone.CommonConfig{}) require.NoError(t, err) assert.Same(t, first, second, "every instance should draw from the one factory") diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go index ff2d8eadd..8b3bfa56a 100644 --- a/libs/standalone/ocr/dependency.go +++ b/libs/standalone/ocr/dependency.go @@ -25,22 +25,24 @@ package ocr import ( "io" - "time" - "github.com/smartcontractkit/libocr/networking" + "github.com/smartcontractkit/libocr/commontypes" ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - - "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" - "github.com/smartcontractkit/chainlink-common/pkg/config" - "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" ) -// OCRFactories bundles the libocr OCR networking factories the caller serves or drives. Any of the -// forms above produces it; the caller does not need to know which. Close tears down the underlying -// peer (a hosted one) or the proxy client connections (a delegating one), and nothing at all for an -// embedded instance, which holds neither. -type OCRFactories struct { +// Factories is what every form of this dependency resolves, and the only part of it both kinds of +// caller need: the transports, and the identity behind them. +// +// It is a type of its own so that the two kinds do not have to share the rest. A process hosting a +// peer serves rage networking to others and signs on their behalf; a process delegating to one +// drives an oracle over that networking. Neither has any use for the other's half, and folding both +// into one struct meant every caller was handed fields that were nil for the form it had - which is +// not a shape a caller can read. +// +// Close tears down the underlying peer (a hosted one) or the proxy client connections (a delegating +// one), and nothing at all for an embedded instance, which holds neither. +type Factories struct { // OCR2Endpoint creates OCR2 BinaryNetworkEndpoints. OCR2Endpoint ocr2types.BinaryNetworkEndpointFactory // OCR3_1Endpoint creates OCR3.1 BinaryNetworkEndpoint2s. @@ -52,57 +54,64 @@ type OCRFactories struct { // registry metadata must know which node it is. PeerID ragetypes.PeerID - // TransmitAccount is the account the node is registered to transmit from, the third part of the - // identity an OCR configuration lists after the peer ID and the public keys. Resolved with them - // because it is the same identity seen from a third side, and an oracle that has two of the - // three is one libocr does not recognise. - // - // Empty for an embedded run, which joins no configuration written by anyone else. - TransmitAccount ocr2types.Account - - // Keyrings sign this oracle's protocol messages and its reports. Resolved with the networking - // because they come from the same place: whoever holds the node's identity holds both. - Keyrings - closer io.Closer } +// NewFactories returns the transport a peer or a proxy provides, with closer as what Close releases - +// the peer itself, or the connections to the process hosting it. +// +// Exported for the hosting form, which lives in libs/standalone/rage: it builds a real peer and has +// to hand back this same shape, and what Close releases is not something a caller should be able to +// forget to set. +func NewFactories( + ocr2 ocr2types.BinaryNetworkEndpointFactory, + ocr31 ocr2types.BinaryNetworkEndpoint2Factory, + peerID ragetypes.PeerID, + closer io.Closer, +) Factories { + return Factories{OCR2Endpoint: ocr2, OCR3_1Endpoint: ocr31, PeerID: peerID, closer: closer} +} + // Close releases the underlying peer or proxy clients. -func (f *OCRFactories) Close() error { +func (f *Factories) Close() error { if f == nil || f.closer == nil { return nil } return f.closer.Close() } -// RageFactories is OCRFactories plus what only a real, locally hosted peer can give: a factory for -// DON-to-DON peer groups over that same rage connection, and the keyring to sign with the same key -// it uses. don2don.Dispatcher takes both. -type RageFactories struct { - OCRFactories +// OCRFactories is Factories plus everything else running an oracle takes: the identity a +// configuration lists it under, the keys it signs with, the peers to dial before it has heard of +// anyone, and the configuration itself. +// +// All of it is the node's rather than the capability's, which is why it is resolved here: a +// capability is told what it runs as. What it runs *under* - the OCR configuration - is a registry +// question and comes from there instead (see libs/standalone/capability), because whoever read the +// registry is the only one that can say. +type OCRFactories struct { + Factories - // PeerGroup creates DON-to-DON peer groups. - PeerGroup networking.PeerGroupFactory - // Keyring signs with the same P2P key the peer above uses, for don2don.Dispatcher's - // message-level signatures. - Keyring ragetypes.PeerKeyring + // TransmitAccount is the account the node is registered to transmit from, the third part of the + // identity an OCR configuration lists after the peer ID and the public keys. Resolved with them + // because it is the same identity seen from a third side, and an oracle that has two of the + // three is one libocr does not recognise. + // + // Empty for a process that runs no oracle and so has no account to report. + TransmitAccount ocr2types.Account - // OCR2 is the node's OCR key bundle, which this process signs with on behalf of oracles that - // hold no keys of their own. Nil for an embedded run, which has no node keystore to take one - // from. - OCR2 ocr2key.KeyBundle -} + // Keyrings sign this oracle's protocol messages and its reports. Resolved with the networking + // because they come from the same place: whoever holds the node's identity holds both - or, for + // an embedded instance, derives both from its index. + Keyrings -// defaultPeerConfig is the peer settings the flags are bound to and decoded into, so an unset -// setting keeps the value it is given here. Freshly allocated per call rather than shared, so two -// dependencies (or two instances of one) can never decode into the same peer settings. -func defaultPeerConfig() *ocrcommon.Config { - return &ocrcommon.Config{ - DeltaReconcile: *config.MustNewDuration(time.Minute), - DeltaDial: *config.MustNewDuration(5 * time.Second), - IncomingBufferSize: 100, - OutgoingBufferSize: 100, - } + // Bootstrappers are the peers to dial before this oracle has heard of anyone. A configuration + // says who the DON is, not where to find it, so this is configured alongside the networking it + // is dialled over - and not by the capability, which would be answering a question about a + // network it is deliberately kept away from. + // + // Empty for an embedded run: its peers are goroutines in this process, so there is nothing to + // dial and nothing to be told. + Bootstrappers []commontypes.BootstrapperLocator } // multiCloser closes several io.Closers, returning the first error. diff --git a/libs/standalone/ocr/embed.go b/libs/standalone/ocr/embed.go index 4a2a18591..c65be093b 100644 --- a/libs/standalone/ocr/embed.go +++ b/libs/standalone/ocr/embed.go @@ -2,42 +2,74 @@ package ocr import ( "context" + "fmt" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" ) -// embedded is one embedded instance's rage networking: an identity derived from the instance index -// and the in-process transport (see inproc.go and keyring.go). +// EmbeddedFactories is the transport of instance i of an embedded run: an identity derived from the +// index, over the in-process network (see inproc.go and keyring.go). // -// It is what both Host and Proxy return from ForEmbedding, because embedding erases the difference +// It is what embedding replaces both configured forms with, because embedding erases the difference // between them. Hosting a peer or delegating to one is a question about a network, and there is no // network here: the peers an embedded instance talks to are goroutines in the same process, reached // over channels. Delegating that to a proxy would mean serialising a message so a gRPC connection to // this process could hand it back. // -// So this needs no configuration at all: no listen address, no proxy address, no peer ID, and above -// all no keystore password, since the identity is derived rather than unlocked. That is why the -// settings those forms do need are checked when they are resolved rather than tagged `required` - -// an embedded instance would have to be given values it has no use for. +// Exported for the hosting form, which is in another package (libs/standalone/rage) and resolves the +// same transport for an embedded run. +// +// Nothing in it needs closing: the endpoints are closed by whoever created them, and the transport +// itself holds no resource beyond the maps they register in. +func EmbeddedFactories(lggr commonlogger.Logger, i int) (Factories, error) { + keyring, err := DeterministicKeyring(i) + if err != nil { + return Factories{}, err + } + peerID := ragetypes.PeerIDFromKeyring(keyring) + + lggr.Infow("Using in-process rage networking", "instance", i, "peerID", peerID.String()) + + return Factories{ + OCR2Endpoint: ocr2Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, + OCR3_1Endpoint: ocr31Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, + PeerID: peerID, + }, nil +} + +// embedded is one embedded instance's OCR dependency: the in-process transport, the keys it signs +// with, and the account the configuration of this run lists it under. +// +// So it needs almost no configuration: no listen address, no proxy address, no peer ID, no account, +// no bootstrap addresses, and above all no keystore password, since the identity is derived rather +// than unlocked. That is why the settings the configured forms do need are checked when they are +// resolved rather than tagged `required` - an embedded instance would have to be given values it has +// no use for. What is left is the protocol itself: see embeddedOCRConfig. type embedded struct { lggr commonlogger.Logger index int + + // instances is how many there are, which is the DON these oracles form. Kept only to check this + // instance is in it: an index outside the DON is a run whose --instances disagrees with itself, + // and saying so beats spending the run as a member no configuration lists. + instances int } -var _ standalone.BootstrapDependency[*RageFactories] = (*embedded)(nil) +var _ standalone.BootstrapDependency[*OCRFactories] = (*embedded)(nil) -// Namespace is the same ocr.* the configured forms use, though it names nothing: there is no -// configuration here to root under it. +// Namespace is the same ocr.* the configured forms use: an embedded instance is still an oracle, and +// what little it is configured with is the protocol it runs. func (d *embedded) Namespace() string { return "ocr" } -// Config is nil: there is nothing to configure. Which is the point - an embedded instance cannot be -// told a listen address, a proxy address, a peer ID or a keystore password, so `embed` does not -// offer them. -func (d *embedded) Config() any { return nil } +// Config is the protocol these oracles run, defaulted: the real shared configuration, so anything +// libocr can be told about a round is a flag here too. Who the members are and what the digest is +// are not in it - see EmbeddedOCRConfig, which fills both in. +func (d *embedded) Config() any { return &DefaultEmbeddedOCRConfig } func (d *embedded) Dependencies() []standalone.BootstrapCommand { // No database: the identity is derived, and there are no announcements to store when every peer @@ -47,51 +79,41 @@ func (d *embedded) Dependencies() []standalone.BootstrapCommand { // ForEmbedding returns the dependency of instance i, so an already-embedded dependency embedded // again is that instance's rather than a nesting of them. -func (d *embedded) ForEmbedding(i int) standalone.BootstrapDependency[*RageFactories] { - return &embedded{lggr: d.lggr, index: i} +func (d *embedded) ForEmbedding(i, instances int) standalone.BootstrapDependency[*OCRFactories] { + return &embedded{lggr: d.lggr, index: i, instances: instances} } -func (d *embedded) Get(context.Context, standalone.CommonConfig) (*RageFactories, error) { - keyring, err := DeterministicKeyring(d.index) +func (d *embedded) Get(context.Context, standalone.CommonConfig) (*OCRFactories, error) { + if d.index >= d.instances { + return nil, fmt.Errorf("instance %d is not one of the %d instances of this run, so no configuration this run computes will list it", + d.index, d.instances) + } + + factories, err := EmbeddedFactories(d.lggr, d.index) if err != nil { return nil, err } - peerID := ragetypes.PeerIDFromKeyring(keyring) - d.lggr.Infow("Using in-process rage networking", - "instance", d.index, - "peerID", peerID.String(), - ) - - // Nothing here is closed: the endpoints are closed by whoever created them, and the transport - // itself holds no resource beyond the maps they register in. PeerGroup and Keyring are left - // unset: there is no in-process peer group simulation, so an embedded instance cannot host - // don2don.Dispatcher. - return &RageFactories{ - OCRFactories: OCRFactories{ - OCR2Endpoint: ocr2Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, - OCR3_1Endpoint: ocr31Factory{net: embedNetwork, peerID: peerID.String(), bufferSize: defaultBufferSize}, - PeerID: peerID, - }, - }, nil -} - -// embeddedOCR adapts embedded to standalone.BootstrapDependency[*OCRFactories], for Proxy's -// ForEmbedding: a delegating process only ever needs the OCR half. -type embeddedOCR struct { - *embedded -} - -var _ standalone.BootstrapDependency[*OCRFactories] = embeddedOCR{} - -func (d embeddedOCR) ForEmbedding(i int) standalone.BootstrapDependency[*OCRFactories] { - return embeddedOCR{&embedded{lggr: d.lggr, index: i}} -} - -func (d embeddedOCR) Get(ctx context.Context, cc standalone.CommonConfig) (*OCRFactories, error) { - f, err := d.embedded.Get(ctx, cc) + // Its own bundle, for the same reason it derives its own peer identity: there is no node keystore + // behind an embedded instance and so nothing to sign on its behalf. Which also means it signs + // here rather than asking a proxy - what the proxy form does is reach a key it does not have. + bundle, err := EmbeddedOCR2Bundle(d.index) + if err != nil { + return nil, err + } + onchain, err := ocr2key.NewOCR3Keyring(evmFamily, bundle) if err != nil { return nil, err } - return &f.OCRFactories, nil + + return &OCRFactories{ + Factories: factories, + // Unlike a delegating run, an embedded instance does have an account of its own to report: + // the configuration it joins is the one built over this process's instances (see + // EmbeddedOCRConfig), which lists it under exactly this account. + TransmitAccount: EmbeddedTransmitAccount(bundle), + Keyrings: Keyrings{Offchain: bundle, Onchain: onchain}, + // Bootstrappers stays empty: an embedded instance's peers are goroutines beside it, so there + // is nothing to dial and no address anyone could be told. + }, nil } diff --git a/libs/standalone/ocr/embed_config.go b/libs/standalone/ocr/embed_config.go new file mode 100644 index 000000000..b444a4c12 --- /dev/null +++ b/libs/standalone/ocr/embed_config.go @@ -0,0 +1,320 @@ +package ocr + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "golang.org/x/crypto/curve25519" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + libsocr "github.com/smartcontractkit/capabilities/libs/ocr" + "github.com/smartcontractkit/capabilities/libs/standalone/capability" +) + +// This file is the OCR configuration of an embedded run: the one thing the in-process transport +// cannot replace. +// +// Everything else embedding needs it settles per instance - an identity of its own (keyring.go) over +// a transport that goes nowhere (inproc.go). A configuration is different because it is not one +// instance's to decide: it says who the DON is, and every member has to arrive at the same answer +// byte for byte or their digests differ and they never speak. What makes that possible here is that +// the members share a process, so the keys they sign with are in reach of all of them (see +// embedBundles) and the only thing left to say is how many there are. +// +// It is assembled exactly as the node's registry assembles one - libocr's own serialisation, and +// libsocr.ConfigDigest over the result - rather than being a second scheme that only embedded runs +// use: an embedded oracle should be joining a real configuration, one it merely computed itself. + +const ( + // embeddedChainID and embeddedRegistryAddress stand in for the chain and contract a real + // configuration was read from. They go into the digest, which is why they cannot simply be + // left empty: the digest is what stops a configuration being replayed against another + // registry, so an embedded run names itself rather than naming nothing - a digest computed + // here is then valid only for an embedded run, and no configuration a node wrote can collide + // with one. + embeddedChainID = 0 + embeddedRegistryAddress = "cre/standalone/embed" + + // embeddedConfigCount is the configuration's counter. An embedded DON's configuration never + // changes - it follows from the instance count, which is fixed for the run - so this is the + // first and only one. + embeddedConfigCount = 1 +) + +// DefaultEmbeddedOCRConfig is the protocol an embedded run's oracles run under, and the struct the +// embed command's ocr.* settings are decoded into. +// +// It is libocr's own shared configuration rather than a summary of it, so every knob a real +// deployment has, an embedded run has too, under the name libocr gives it. Three fields are not +// settings: OracleIdentities is the run's instances, ConfigDigest is computed from the rest, and F +// defaults to the largest value the instance count allows (see EmbeddedOCRConfig). +// +// The values are brisk rather than production-safe: an embed run is watched by whoever started it, +// and a round every couple of seconds is what makes it worth watching. +// +// Package-level because every instance runs the same protocol - they are one DON - and because what +// changes it is a flag rather than a caller. +var DefaultEmbeddedOCRConfig = ocr3confighelper.PublicConfig{ + DeltaProgress: 2 * time.Second, + DeltaResend: 2 * time.Second, + DeltaInitial: 500 * time.Millisecond, + DeltaRound: time.Second, + DeltaGrace: 500 * time.Millisecond, + DeltaCertifiedCommitRequest: 500 * time.Millisecond, + DeltaStage: 5 * time.Second, + RMax: 5, + + MaxDurationQuery: time.Second, + MaxDurationObservation: time.Second, + MaxDurationShouldAcceptAttestedReport: time.Second, + MaxDurationShouldTransmitAcceptedReport: time.Second, +} + +// init hands the builder below to the capability dependency, which is where an OCR-based capability +// finds its configuration however the run was started. +func init() { + capability.RegisterEmbeddedOCRConfig(EmbeddedOCRConfigRegistry) +} + +// EmbeddedOCRConfigRegistry returns the core.OCRConfigRegistry an embedded run's oracles read their +// configuration from. +// +// It is a registry in the sense that matters to an oracle - it answers what configuration a +// capability runs under, digest included - while holding no snapshot and reading no contract: there +// is no node behind an embedded run to read one. What it answers with is the DON of oracles +// EmbeddedOCRConfig describes. +// +// oracles is how many instances the run has, which is the oracle set: instance i of that many is the +// i-th member. It cannot be discovered from here, which is why it is asked for - see the embedded +// capability dependency, which takes it as a setting. +func EmbeddedOCRConfigRegistry(oracles int) core.OCRConfigRegistry { + return embeddedRegistry{oracles: oracles} +} + +// embeddedRegistry answers with the configuration of the embedded DON, whatever is asked about: the +// capability ID, DON ID and key are what the digest is computed over rather than what a record is +// looked up by, since an embedded run holds no records. +type embeddedRegistry struct { + oracles int +} + +var _ core.OCRConfigRegistry = embeddedRegistry{} + +func (r embeddedRegistry) OCRConfig(_ context.Context, capabilityID string, donID uint32, key string) (ocrtypes.ContractConfig, error) { + return EmbeddedOCRConfig(capabilityID, donID, key, r.oracles) +} + +// EmbeddedOCRConfig is the OCR3 configuration a DON of oracles embedded instances runs under, as +// every one of them computes it. +// +// The same call in the same process always answers the same thing, which is what the instances rely +// on: their oracle set is this process's instances, so the configuration is over this process's keys +// (embedBundles) and is stable for as long as they are. Another process, or another run, gets a +// configuration of its own - there is nothing an embedded run outlives. +// +// Exported so a test can ask what configuration the instances it starts will run under, and check +// what it sees against it. +// +// F is the largest fault tolerance the oracle count allows (3F < N), which is what a DON of this +// size would be configured with. One instance therefore runs at F=0, which is the honest answer for +// a DON that cannot tolerate a fault rather than a refusal to run. +func EmbeddedOCRConfig(capabilityID string, donID uint32, key string, oracles int) (ocrtypes.ContractConfig, error) { + if oracles < 1 { + return ocrtypes.ContractConfig{}, fmt.Errorf("cannot build an OCR config for %d oracles: at least one is required", oracles) + } + // A capability normally runs one OCR instance, and naming it is a detail only a capability + // running several has to care about - the same default the node's registry applies, so that a + // capability asking for its only instance gets the same answer either way. + if key == "" { + key = capabilitiespb.OCR3ConfigDefaultKey + } + + identities, err := embeddedIdentities(oracles) + if err != nil { + return ocrtypes.ContractConfig{}, err + } + + cfg, err := embeddedOCR3Config(DefaultEmbeddedOCRConfig, identities) + if err != nil { + return ocrtypes.ContractConfig{}, err + } + + // The same digest function the node's registry uses, over a chain and address that name this + // run: what an oracle checks is that every member agrees, and computing it the one way is what + // makes the embedded path exercise the real one. + digest, err := libsocr.ConfigDigest(embeddedChainID, embeddedRegistryAddress, capabilityID, donID, key, cfg) + if err != nil { + return ocrtypes.ContractConfig{}, fmt.Errorf("failed to compute the OCR config digest for embedded capability %s: %w", capabilityID, err) + } + return capabilitiespb.OCR3ConfigFromProto(cfg, digest) +} + +// embeddedIdentities is the DON, in instance order: instance i is oracle i, listed under the same +// peer ID, keys and account instance i resolves for itself when it starts. +func embeddedIdentities(oracles int) ([]confighelper.OracleIdentityExtra, error) { + identities := make([]confighelper.OracleIdentityExtra, 0, oracles) + for i := range oracles { + peerID, err := DeterministicPeerID(i) + if err != nil { + return nil, err + } + bundle, err := EmbeddedOCR2Bundle(i) + if err != nil { + return nil, err + } + // The multichain form, which is what a capability DON's configuration lists members by and + // therefore what libocr compares an oracle's own keyring against - see + // ocr2key.NewOCR3Keyring, which is what an embedded instance signs with. + onchainPublicKey, err := marshalEVMOnchainPublicKey(bundle.PublicKey()) + if err != nil { + return nil, fmt.Errorf("failed to encode the onchain public key of instance %d: %w", i, err) + } + + identities = append(identities, confighelper.OracleIdentityExtra{ + OracleIdentity: confighelper.OracleIdentity{ + OffchainPublicKey: bundle.OffchainPublicKey(), + OnchainPublicKey: onchainPublicKey, + PeerID: peerID.String(), + TransmitAccount: EmbeddedTransmitAccount(bundle), + }, + ConfigEncryptionPublicKey: bundle.ConfigEncryptionPublicKey(), + }) + } + return identities, nil +} + +// EmbeddedTransmitAccount is the account an embedded instance transmits as: its own onchain signing +// key, hex encoded, which is the form a registry's transmitters are read as. +// +// An account is part of the identity a configuration lists, and libocr checks it like the rest: an +// oracle whose account does not match its entry is not recognised as a member at all. So an embedded +// instance has to have one, it has to be its own, and it has to be something the configuration can +// name without being told - which the key it already derives is. +func EmbeddedTransmitAccount(bundle ocr2key.KeyBundle) ocrtypes.Account { + return ocrtypes.Account(hex.EncodeToString(bundle.PublicKey())) +} + +// embeddedOCR3Config assembles the configuration in the form the registry stores it, which is what +// the digest is computed over. The offchain half - the deltas, the peer IDs, the offchain public +// keys and the shared secret encrypted to each member - is serialised by libocr itself, so an +// embedded configuration is the same bytes a real one would be. +func embeddedOCR3Config(public ocr3confighelper.PublicConfig, identities []confighelper.OracleIdentityExtra) (*capabilitiespb.OCR3Config, error) { + schedule := public.S + if len(schedule) == 0 { + // One stage holding every oracle, so any of them may transmit. An embedded run has nowhere to + // transmit to but the caller in its own process, so there is nothing a staggered schedule + // would spare. + schedule = []int{len(identities)} + } + + // The two fields of the real configuration an embedded run fills in itself. Offered as flags + // because this is the real struct, and refused rather than ignored: a setting that is silently + // overwritten is worse than one that is missing. + if len(public.OracleIdentities) > 0 { + return nil, errors.New("who the members of an embedded DON are follows from --instances, so --ocr.oracle-identities cannot be set") + } + if public.ConfigDigest != (ocrtypes.ConfigDigest{}) { + return nil, errors.New("an embedded run computes its own config digest, so --ocr.config-digest cannot be set") + } + + f, err := embeddedF(public.F, len(identities)) + if err != nil { + return nil, err + } + + signers, _, fOut, onchainConfig, offchainConfigVersion, offchainConfig, err := ocr3confighelper.ContractSetConfigArgsDeterministic( + embeddedEphemeralSecretKey(), + embeddedSharedSecret(), + + public.DeltaProgress, + public.DeltaResend, + public.DeltaInitial, + public.DeltaRound, + public.DeltaGrace, + public.DeltaCertifiedCommitRequest, + public.DeltaStage, + public.RMax, + schedule, + identities, + public.ReportingPluginConfig, + public.MaxDurationInitialization, + public.MaxDurationQuery, + public.MaxDurationObservation, + public.MaxDurationShouldAcceptAttestedReport, + public.MaxDurationShouldTransmitAcceptedReport, + f, + public.OnchainConfig, + ) + if err != nil { + return nil, fmt.Errorf("failed to build the embedded OCR config: %w", err) + } + f_ := fOut + + cfg := &capabilitiespb.OCR3Config{ + F: uint32(f_), + OnchainConfig: onchainConfig, + OffchainConfigVersion: offchainConfigVersion, + OffchainConfig: offchainConfig, + ConfigCount: embeddedConfigCount, + } + for i, signer := range signers { + cfg.Signers = append(cfg.Signers, signer) + // The registry stores an account as the bytes it hex encodes to, which is the encoding + // EmbeddedTransmitAccount produces and the one OCR3ConfigFromProto reverses - so the + // account an oracle reports and the one the configuration lists are the same string. + account, derr := hex.DecodeString(string(identities[i].TransmitAccount)) + if derr != nil { + return nil, fmt.Errorf("failed to decode the transmit account of oracle %d: %w", i, derr) + } + cfg.Transmitters = append(cfg.Transmitters, account) + } + return cfg, nil +} + +// embeddedF is the fault tolerance the run is configured with. +// +// Unset - which is to say zero, since a DON of one tolerates nothing anyway - means the largest value +// the oracle count allows, so a run of four is a run that survives one bad member without anyone +// having to work out that 3F < N. A value that was asked for is checked rather than corrected: an +// oracle set that cannot support the F it was given is a configuration libocr would reject later, and +// later is a worse place to hear it. +func embeddedF(f, n int) (int, error) { + if f == 0 { + return (n - 1) / 3, nil + } + if f < 0 || 3*f >= n { + return 0, fmt.Errorf("F of %d is not possible for %d oracles: libocr requires a non-negative F with 3F < N", f, n) + } + return f, nil +} + +// embeddedSharedSecret is the secret an embedded DON's members derive their leaders and transmitters +// from, and embeddedEphemeralSecretKey the key it is encrypted to each of them under. +// +// Both are constants, hashed from a label. libocr calls the shared secret a low-value secret - +// knowing it says who will lead a round early, and nothing else - and an embedded run's keys are all +// public by construction anyway. What they must be is identical across instances, since they are +// part of the configuration and therefore of its digest, and a random one per instance would leave +// every instance with a DON of one. + +func embeddedSharedSecret() [16]byte { + // Truncated to the 128-bit key libocr's shared secret is. + hash := sha256.Sum256([]byte("cre/standalone/embed/ocr3/shared-secret")) + var secret [16]byte + copy(secret[:], hash[:]) + return secret +} + +func embeddedEphemeralSecretKey() [curve25519.ScalarSize]byte { + return sha256.Sum256([]byte("cre/standalone/embed/ocr3/ephemeral-key")) +} diff --git a/libs/standalone/ocr/embed_config_test.go b/libs/standalone/ocr/embed_config_test.go new file mode 100644 index 000000000..55eed5772 --- /dev/null +++ b/libs/standalone/ocr/embed_config_test.go @@ -0,0 +1,129 @@ +package ocr + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +const testCapabilityID = "consensus@1.0.0-alpha" + +func TestEmbeddedOCRConfig(t *testing.T) { + t.Run("is one configuration every instance computes identically", func(t *testing.T) { + first, err := EmbeddedOCRConfig(testCapabilityID, 1, "", 4) + require.NoError(t, err) + second, err := EmbeddedOCRConfig(testCapabilityID, 1, "", 4) + require.NoError(t, err) + + // The digest is the whole point: oracles that compute different ones never speak, and they + // each compute their own. + assert.Equal(t, first.ConfigDigest, second.ConfigDigest) + assert.Equal(t, first.Signers, second.Signers) + assert.Equal(t, first.Transmitters, second.Transmitters) + assert.Equal(t, first.OffchainConfig, second.OffchainConfig) + }) + + t.Run("names a different configuration per capability, DON and OCR instance", func(t *testing.T) { + base, err := EmbeddedOCRConfig(testCapabilityID, 1, "", 4) + require.NoError(t, err) + + otherCapability, err := EmbeddedOCRConfig("other@1.0.0", 1, "", 4) + require.NoError(t, err) + otherDON, err := EmbeddedOCRConfig(testCapabilityID, 2, "", 4) + require.NoError(t, err) + otherKey, err := EmbeddedOCRConfig(testCapabilityID, 1, "second", 4) + require.NoError(t, err) + + assert.NotEqual(t, base.ConfigDigest, otherCapability.ConfigDigest) + assert.NotEqual(t, base.ConfigDigest, otherDON.ConfigDigest) + assert.NotEqual(t, base.ConfigDigest, otherKey.ConfigDigest) + }) + + t.Run("is a configuration libocr accepts", func(t *testing.T) { + // One and four because those are the ends of it: one instance is a DON that tolerates no + // fault (F=0), and four is the smallest that tolerates one. + for _, oracles := range []int{1, 2, 3, 4, 7} { + config, err := EmbeddedOCRConfig(testCapabilityID, 1, "", oracles) + require.NoError(t, err) + + // The same decoding an oracle does on the configuration it is handed: the identity lists + // have to agree in length, hold no duplicates, and the protocol parameters have to pass + // libocr's own bounds - all of which this asserts by not erroring. + public, err := ocr3confighelper.PublicConfigFromContractConfig(false, config) + require.NoError(t, err, "%d oracles", oracles) + + assert.Len(t, public.OracleIdentities, oracles) + assert.Equal(t, (oracles-1)/3, public.F) + } + }) + + t.Run("refuses a DON of no oracles", func(t *testing.T) { + _, err := EmbeddedOCRConfig(testCapabilityID, 1, "", 0) + require.ErrorContains(t, err, "at least one is required") + }) +} + +// TestEmbeddedOCRConfigListsEveryInstance is the check that matters most: libocr recognises an +// oracle by matching all four parts of its identity against the configuration, and an instance that +// matches three of them is not a member at all. So this asserts, for what each instance resolves for +// itself, exactly what SharedConfigFromContractConfig asserts. +func TestEmbeddedOCRConfigListsEveryInstance(t *testing.T) { + const oracles = 4 + + config, err := EmbeddedOCRConfig(testCapabilityID, 1, "", oracles) + require.NoError(t, err) + public, err := ocr3confighelper.PublicConfigFromContractConfig(false, config) + require.NoError(t, err) + + for i := range oracles { + // The OCR form, since it is an oracle's identity that a configuration lists. + dep := &embedded{lggr: logger.Test(t), index: i, instances: oracles} + factories, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + + identity := public.OracleIdentities[i] + assert.Equal(t, factories.PeerID.String(), identity.PeerID, "instance %d", i) + assert.Equal(t, factories.TransmitAccount, identity.TransmitAccount, "instance %d", i) + assert.Equal(t, factories.Offchain.OffchainPublicKey(), identity.OffchainPublicKey, "instance %d", i) + // What the keyring answers with is what libocr compares to the signer entry, byte for byte. + assert.True(t, bytes.Equal(factories.Onchain.PublicKey(), identity.OnchainPublicKey), "instance %d", i) + } +} + +// An embedded instance has nothing to dial: its peers are goroutines beside it. +func TestEmbeddedFactoriesHaveNoBootstrappers(t *testing.T) { + dep := &embedded{lggr: logger.Test(t), index: 1, instances: 3} + + factories, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + + assert.Empty(t, factories.Bootstrappers) + assert.NotEmpty(t, factories.TransmitAccount, "an embedded oracle reports the account its configuration lists") +} + +// An instance outside the run it was told about would be a member no configuration lists, and says +// so rather than spending the run unrecognised. +func TestEmbeddedOCRRefusesAnInstanceOutsideTheRun(t *testing.T) { + dep := &embedded{lggr: logger.Test(t), index: 3, instances: 1} + + _, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.ErrorContains(t, err, "instance 3 is not one of the 1 instances") +} + +func TestEmbeddedOCRConfigRegistry(t *testing.T) { + registry := EmbeddedOCRConfigRegistry(4) + + got, err := registry.OCRConfig(t.Context(), testCapabilityID, 1, "") + require.NoError(t, err) + + want, err := EmbeddedOCRConfig(testCapabilityID, 1, "", 4) + require.NoError(t, err) + assert.Equal(t, want, got) +} diff --git a/libs/standalone/ocr/keyring.go b/libs/standalone/ocr/keyring.go index 6e6331e27..7b28b313a 100644 --- a/libs/standalone/ocr/keyring.go +++ b/libs/standalone/ocr/keyring.go @@ -1,102 +1,20 @@ package ocr import ( - "context" "crypto" "crypto/ed25519" "crypto/rand" "crypto/sha256" - "errors" "fmt" - "sort" "strconv" - "strings" - - "github.com/jmoiron/sqlx" + "sync" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - "github.com/smartcontractkit/chainlink-common/keystore/corekeys/models" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" ) -// loadPeerKeyring loads the P2P key from the node's keystore so this process -// uses the SAME peer identity as the node it fronts (other DON members expect -// this node's peer ID at this address). It reads the node's existing encrypted -// key ring (the legacy `encrypted_key_rings` table, in chainlink-common's -// corekeys/models format) and decrypts it with the keystore password. This is a -// deliberately small copy of core's keyManager.Unlock using only -// chainlink-common packages, so this binary needn't import chainlink core. -// -// password is the node's keystore password: this process shares the node's -// database and therefore its keystore password. -// -// TODO: drop this once the keystore is migrated to chainlink-common's -// keystore.Keystore + pgstore (as chainlink-ccv already uses), after which we -// can LoadKeystore from the shared table directly. -func loadPeerKeyring(ctx context.Context, ds *sqlx.DB, password string) (*peerKeyring, error) { - var encrypted []byte - if err := ds.GetContext(ctx, &encrypted, "SELECT encrypted_keys FROM encrypted_key_rings LIMIT 1"); err != nil { - return nil, fmt.Errorf("failed to read node key ring: %w", err) - } - kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(password) - if err != nil { - return nil, fmt.Errorf("failed to decrypt node key ring: %w", err) - } - for _, k := range kr.P2P { - pub, perr := ragetypes.PeerPublicKeyFromGenericPublicKey(k.Public()) - if perr != nil { - return nil, fmt.Errorf("failed to derive peer public key: %w", perr) - } - return &peerKeyring{signer: k, publicKey: pub}, nil - } - return nil, errors.New("no P2P key found in node key ring") -} - -// loadOCR2Bundle loads the node's OCR2 key bundle from the same key ring the P2P -// key came from, so the oracle signing done on a capability's behalf is done -// with the node's own OCR identity - the one the registry lists as a signer. -// -// bundleID names which bundle when the node has several. A node running one -// capability DON usually has one, and naming it then would be one more thing to -// keep in step with the keystore, so an empty bundleID takes the only one there -// is and refuses to guess between several. -func loadOCR2Bundle(ctx context.Context, ds *sqlx.DB, password, bundleID string) (ocr2key.KeyBundle, error) { - var encrypted []byte - if err := ds.GetContext(ctx, &encrypted, "SELECT encrypted_keys FROM encrypted_key_rings LIMIT 1"); err != nil { - return nil, fmt.Errorf("failed to read node key ring: %w", err) - } - kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(password) - if err != nil { - return nil, fmt.Errorf("failed to decrypt node key ring: %w", err) - } - - if bundleID != "" { - bundle, ok := kr.OCR2[bundleID] - if !ok { - return nil, fmt.Errorf("no OCR2 key bundle %q in the node key ring", bundleID) - } - return bundle, nil - } - - switch len(kr.OCR2) { - case 0: - return nil, errors.New("no OCR2 key bundle found in the node key ring") - case 1: - for _, bundle := range kr.OCR2 { - return bundle, nil - } - } - - ids := make([]string, 0, len(kr.OCR2)) - for id := range kr.OCR2 { - ids = append(ids, id) - } - sort.Strings(ids) - return nil, fmt.Errorf("the node key ring holds %d OCR2 key bundles (%s), so --ocr.key-bundle-id must say which one to sign with", - len(ids), strings.Join(ids, ", ")) -} - // deterministicSeedPrefix domain-separates the derived instance keys, so the seeds are specific // to this framework's embed mode and cannot collide with another scheme deriving keys from an // index. Changing it changes every derived peer ID. @@ -116,12 +34,21 @@ const deterministicSeedPrefix = "cre/standalone/instance/" // matters may be protected by one: embed mode is for local runs and tests. func DeterministicKeyring(i int) (ragetypes.PeerKeyring, error) { seed := sha256.Sum256([]byte(deterministicSeedPrefix + strconv.Itoa(i))) - key := ed25519.NewKeyFromSeed(seed[:]) - pub, err := ragetypes.PeerPublicKeyFromGenericPublicKey(key.Public()) + return NewPeerKeyring(ed25519.NewKeyFromSeed(seed[:])) +} + +// NewPeerKeyring returns signer as a rage peer keyring, deriving the public half it announces +// under. +// +// Exported because a P2P key reaches this framework two ways - derived from an instance index here, +// or unlocked from a node keystore (see libs/standalone/rage) - and both end up needing the same +// wrapper. Used in place of the deprecated PeerConfig.PrivKey field. +func NewPeerKeyring(signer crypto.Signer) (ragetypes.PeerKeyring, error) { + pub, err := ragetypes.PeerPublicKeyFromGenericPublicKey(signer.Public()) if err != nil { - return nil, fmt.Errorf("failed to derive peer public key for instance %d: %w", i, err) + return nil, fmt.Errorf("failed to derive the peer public key: %w", err) } - return &peerKeyring{signer: key, publicKey: pub}, nil + return &peerKeyring{signer: signer, publicKey: pub}, nil } // DeterministicPeerID returns the peer ID DeterministicKeyring gives instance i, so a caller @@ -134,6 +61,57 @@ func DeterministicPeerID(i int) (ragetypes.PeerID, error) { return ragetypes.PeerIDFromKeyring(keyring), nil } +// embedBundles is the OCR key bundle of each instance in this process, kept in one place so that +// every instance sees every other instance's. +// +// It is package-level for the reason embedNetwork is (see inproc.go): there is exactly one process, +// so there is exactly one set of instances inside it, and an OCR configuration naming them all has +// to be built from the same keys the instances sign with. That is also why these are generated +// rather than derived from the index the way the P2P identity is - a key derived from an index would +// be reproducible outside the process too, which nothing here needs, and secp256k1 key generation +// ignores the material it is offered anyway (crypto/ecdsa generates from the system CSPRNG, so a +// seeded reader buys nothing). +// +// EVM, because a capability DON's members are registered with an EVM signing key: an embedded run +// should exercise the signing and verification path a real one takes. +var embedBundles = &bundleSet{bundles: map[int]ocr2key.KeyBundle{}} + +// bundleSet hands out one OCR key bundle per instance index, making it the first time it is asked +// for. Which instance asks first does not matter: an instance asks for its own to sign with, and the +// configuration asks for all of them to name the DON, and either order ends up with the same set. +type bundleSet struct { + mu sync.Mutex + bundles map[int]ocr2key.KeyBundle +} + +func (s *bundleSet) get(i int) (ocr2key.KeyBundle, error) { + if i < 0 { + return nil, fmt.Errorf("cannot resolve the OCR key bundle of instance %d: an instance index is not negative", i) + } + + s.mu.Lock() + defer s.mu.Unlock() + + if bundle, ok := s.bundles[i]; ok { + return bundle, nil + } + bundle, err := ocr2key.New(corekeys.EVM) + if err != nil { + return nil, fmt.Errorf("failed to create an OCR key bundle for instance %d: %w", i, err) + } + s.bundles[i] = bundle + return bundle, nil +} + +// EmbeddedOCR2Bundle returns the OCR2 key bundle instance i signs with: its protocol messages with +// the offchain half, and its reports with the onchain one. +// +// An embedded instance has no node keystore to take one from, so the process keeps one for each of +// them - see embedBundles, and EmbeddedOCRConfig for the configuration that lists their public +// halves as the DON. Exported for the hosting form, which serves this bundle where a real one would +// serve the node's (see libs/standalone/rage). +func EmbeddedOCR2Bundle(i int) (ocr2key.KeyBundle, error) { return embedBundles.get(i) } + // peerKeyring is a ragetypes.PeerKeyring backed by a P2P key (a crypto.Signer): the node's own, // loaded from its keystore, or one derived from an instance index. Used in place of the deprecated // PeerConfig.PrivKey field. diff --git a/libs/standalone/ocr/proxy.go b/libs/standalone/ocr/proxy.go index 70aa708a6..b6a620c9c 100644 --- a/libs/standalone/ocr/proxy.go +++ b/libs/standalone/ocr/proxy.go @@ -11,6 +11,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/smartcontractkit/chainlink-common/pkg/config" commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" @@ -45,7 +46,20 @@ type ProxyConfig struct { // name in the OCR configuration this process joins, it is a public value, and the process // holding the keys it belongs to is the one that could look it up. An oracle whose account does // not match its entry in the configuration is not a member of it. - TransmitAccount string `usage:"account this node is registered to transmit from; required outside embed" example:"'0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437'"` + // + // Not required: a process driving no oracle - one that only passes messages over the endpoints - + // has no account, and demanding one would be demanding a name for something that is never named. + // An oracle given the wrong account is rejected by libocr as a non-member, which is where a + // missing one shows up too. + TransmitAccount string `usage:"account this node is registered to transmit from, for a process running an oracle" example:"'0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437'"` + + // Bootstrappers are where the DON's peers can first be reached, which is a property of the + // network this process delegates to rather than of the capability running over it - so it is + // configured here, beside the address of the proxy that will do the dialling. + // + // The peers behind these addresses find each other from here on: a configuration names the + // members, and this is how the first of them is reached to learn the rest. + Bootstrappers config.BootstrapperLocators `usage:"peerID@host:port of the DON's bootstrap peers, dialled to reach the rest of it" example:"['12D3KooWFirst@127.0.0.1:6690']"` } // Proxy returns a standalone.BootstrapDependency that resolves the libocr Factories from proxy @@ -83,8 +97,8 @@ func (d *proxyDependency) Dependencies() []standalone.BootstrapCommand { // hop between instances of one process, since the peer this would delegate to is a goroutine beside // it and delegating would mean serialising a message so a gRPC connection to this process could hand // it back. None of this dependency's settings survive into it - see embedded. -func (d *proxyDependency) ForEmbedding(i int) standalone.BootstrapDependency[*OCRFactories] { - return embeddedOCR{&embedded{lggr: d.lggr, index: i}} +func (d *proxyDependency) ForEmbedding(i, instances int) standalone.BootstrapDependency[*OCRFactories] { + return &embedded{lggr: d.lggr, index: i, instances: instances} } func (d *proxyDependency) Get(ctx context.Context, _ standalone.CommonConfig) (*OCRFactories, error) { @@ -94,9 +108,6 @@ func (d *proxyDependency) Get(ctx context.Context, _ standalone.CommonConfig) (* if d.cfg.PeerID == (ragetypes.PeerID{}) { return nil, errors.New("--ocr.peer-id is required to delegate rage networking") } - if d.cfg.TransmitAccount == "" { - return nil, errors.New("--ocr.transmit-account is required to delegate rage networking") - } peerID := d.cfg.PeerID // The raw peer ID is passed to the endpoint factories, as libocr compares it against the peer @@ -131,11 +142,14 @@ func (d *proxyDependency) Get(ctx context.Context, _ standalone.CommonConfig) (* d.lggr.Infow("Delegating rage networking to proxy", "proxyAddress", d.cfg.ProxyAddress, "peerID", peerID.String()) return &OCRFactories{ - OCR2Endpoint: endpointFactory, - OCR3_1Endpoint: endpoint2Factory, - PeerID: peerID, + Factories: Factories{ + OCR2Endpoint: endpointFactory, + OCR3_1Endpoint: endpoint2Factory, + PeerID: peerID, + closer: multiCloser{endpointFactory, endpoint2Factory, conn}, + }, TransmitAccount: ocr2types.Account(d.cfg.TransmitAccount), Keyrings: Keyrings{Offchain: keyring, Onchain: keyring}, - closer: multiCloser{endpointFactory, endpoint2Factory, conn}, + Bootstrappers: d.cfg.Bootstrappers.ToBootstrapperLocators(), }, nil } diff --git a/libs/standalone/ocr/proxy_test.go b/libs/standalone/ocr/proxy_test.go index a003c3dd5..91bc82e9b 100644 --- a/libs/standalone/ocr/proxy_test.go +++ b/libs/standalone/ocr/proxy_test.go @@ -57,7 +57,7 @@ func TestProxy(t *testing.T) { require.Error(t, decoded.UnmarshalText([]byte("not-a-peer-id"))) }) - t.Run("delegating needs the proxy address, the peer ID and the transmit account", func(t *testing.T) { + t.Run("delegating needs the proxy address and the peer ID", func(t *testing.T) { _, err := (&proxyDependency{lggr: logger.Test(t)}).Get(t.Context(), standalone.CommonConfig{}) require.ErrorContains(t, err, "--ocr.proxy-address is required") @@ -65,25 +65,21 @@ func TestProxy(t *testing.T) { Get(t.Context(), standalone.CommonConfig{}) require.ErrorContains(t, err, "--ocr.peer-id is required") - // Reported here rather than left to libocr: an oracle with no account is one the - // configuration does not list, which surfaces as a member no one recognises rather than as - // something missing. - _, err = (&proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ - ProxyAddress: "127.0.0.1:50051", - PeerID: mustPeerID(t, 7), - }}).Get(t.Context(), standalone.CommonConfig{}) - require.ErrorContains(t, err, "--ocr.transmit-account is required") + // The transmit account is not among them: a process passing messages over the endpoints runs + // no oracle and has no account, and an oracle given none is rejected by libocr as a + // non-member - which is where a wrong one shows up too. }) t.Run("an embedded instance needs neither, deriving its identity instead", func(t *testing.T) { - // Nothing is configured here: embedding a delegating dependency yields one with no settings - // at all, the same form a hosted peer yields. - dep := Proxy(logger.Test(t)).ForEmbedding(2) + // Nothing about where to reach anyone is configured: the identity is derived from the index, + // and the only settings left are the protocol the oracles run. + dep := Proxy(logger.Test(t)).ForEmbedding(2, 4) factories, err := dep.Get(t.Context(), standalone.CommonConfig{}) require.NoError(t, err) assert.Equal(t, mustPeerID(t, 2), factories.PeerID) + assert.Equal(t, &DefaultEmbeddedOCRConfig, dep.Config(), "the protocol is still configurable") }) } diff --git a/libs/standalone/rage/embed.go b/libs/standalone/rage/embed.go new file mode 100644 index 000000000..afeb81e26 --- /dev/null +++ b/libs/standalone/rage/embed.go @@ -0,0 +1,68 @@ +package rage + +import ( + "context" + + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +// embedded is one embedded instance's stand-in for a hosted peer: the in-process transport, and the +// OCR key bundle this process would have signed with on other oracles' behalf. +// +// There is no peer here to host. The instances an embedded run talks to are goroutines beside it, so +// there is nothing to listen on, announce to or discover, and no keystore to unlock an identity from - +// the identity comes from the instance index instead (see ocr.EmbeddedFactories). Which is why this +// form has no settings at all: everything the hosting form is configured with, an embedded instance +// either derives or has no use for. +type embedded struct { + lggr commonlogger.Logger + index int +} + +var _ standalone.BootstrapDependency[*Factories] = (*embedded)(nil) + +// Namespace is the same ocr.* the hosting form uses, though it names nothing: there is no +// configuration here to root under it. +func (d *embedded) Namespace() string { return "ocr" } + +// Config is nil: there is nothing to configure. Which is the point - an embedded instance cannot be +// told a listen address or a keystore password, so `embed` does not offer them. +func (d *embedded) Config() any { return nil } + +func (d *embedded) Dependencies() []standalone.BootstrapCommand { + // No database: the identity is derived, and there are no announcements to store when every peer + // is in this process. + return []standalone.BootstrapCommand{} +} + +// ForEmbedding returns the dependency of instance i, so an already-embedded dependency embedded +// again is that instance's rather than a nesting of them. How many instances there are does not +// matter here: hosting a peer is about a network, and this instance's transport is the same whoever +// else is running. +func (d *embedded) ForEmbedding(i, _ int) standalone.BootstrapDependency[*Factories] { + return &embedded{lggr: d.lggr, index: i} +} + +// Get resolves the transport and this instance's bundle. +// +// PeerGroup and Keyring are left unset: there is no in-process peer group simulation, so an embedded +// instance cannot host don2don.Dispatcher. +func (d *embedded) Get(context.Context, standalone.CommonConfig) (*Factories, error) { + factories, err := ocr.EmbeddedFactories(d.lggr, d.index) + if err != nil { + return nil, err + } + + // The bundle a hosted peer would have taken from the node's keystore. Serving it is all this form + // can still do with it - an oracle in this process holds its own, derived the same way. + bundle, err := ocr.EmbeddedOCR2Bundle(d.index) + if err != nil { + return nil, err + } + + return &Factories{Factories: factories, OCR2: bundle}, nil +} diff --git a/libs/standalone/rage/factories.go b/libs/standalone/rage/factories.go new file mode 100644 index 000000000..f6900af37 --- /dev/null +++ b/libs/standalone/rage/factories.go @@ -0,0 +1,53 @@ +// Package rage is the standalone.BootstrapDependency a binary uses to host the node's libocr rage +// peer: the real thing that listens, dials, announces and signs. +// +// It is separate from libs/standalone/ocr, which is what a binary driving an oracle over someone +// else's peer takes, because the two need different halves of the world and only one of them is +// expensive. Hosting a peer means unlocking the node's keystore, which holds a key of every kind the +// node has and brings every one of those chains' libraries with it. Delegating to a peer means +// verifying signatures and reading a configuration, and nothing else. Keeping them apart is what +// stops a binary that hosts a trigger from linking a cosmos SDK. +package rage + +import ( + "time" + + "github.com/smartcontractkit/libocr/networking" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +// Factories is ocr.Factories plus what only a real, locally hosted peer can give: a factory for +// DON-to-DON peer groups over that same rage connection, and the keyring to sign with the same key +// it uses. don2don.Dispatcher takes both. +type Factories struct { + ocr.Factories + + // PeerGroup creates DON-to-DON peer groups. + PeerGroup networking.PeerGroupFactory + // Keyring signs with the same P2P key the peer above uses, for don2don.Dispatcher's + // message-level signatures. + Keyring ragetypes.PeerKeyring + + // OCR2 is the node's OCR key bundle, which this process signs with on behalf of oracles that + // hold no keys of their own. An embedded instance's is its own rather than a node's, since there + // is no node behind it to borrow one from. + OCR2 ocr2key.KeyBundle +} + +// defaultPeerConfig is the peer settings the flags are bound to and decoded into, so an unset +// setting keeps the value it is given here. Freshly allocated per call rather than shared, so two +// dependencies (or two instances of one) can never decode into the same peer settings. +func defaultPeerConfig() *ocrcommon.Config { + return &ocrcommon.Config{ + DeltaReconcile: *config.MustNewDuration(time.Minute), + DeltaDial: *config.MustNewDuration(5 * time.Second), + IncomingBufferSize: 100, + OutgoingBufferSize: 100, + } +} diff --git a/libs/standalone/ocr/host.go b/libs/standalone/rage/host.go similarity index 86% rename from libs/standalone/ocr/host.go rename to libs/standalone/rage/host.go index 5d6e8e6d1..a2a2f71d2 100644 --- a/libs/standalone/ocr/host.go +++ b/libs/standalone/rage/host.go @@ -1,4 +1,4 @@ -package ocr +package rage import ( "context" @@ -13,12 +13,13 @@ import ( "github.com/smartcontractkit/libocr/networking/rageping" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" "github.com/smartcontractkit/chainlink-common/pkg/config" commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/ocrcommon" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" ) // Config is the configuration of a locally hosted libocr peer: the peer's own settings @@ -53,10 +54,10 @@ type Config struct { // table holding p2p announcements. // // An embedded instance resolves neither: see ForEmbedding. -func Host(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*RageFactories] { +func Host(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB], discovererTable string) standalone.BootstrapDependency[*Factories] { // Wrap in OnceBootstrapper so Get (which creates the peer) runs at most once even if several // services resolve this dependency. - return standalone.OnceBootstrapper[*RageFactories](&hostDependency{ + return standalone.OnceBootstrapper[*Factories](&hostDependency{ lggr: lggr, db: db, discovererTable: discovererTable, @@ -72,10 +73,12 @@ type hostDependency struct { cfg Config } -var _ standalone.BootstrapDependency[*RageFactories] = (*hostDependency)(nil) +var _ standalone.BootstrapDependency[*Factories] = (*hostDependency)(nil) // Namespace groups the libocr networking settings under ocr.* (--ocr.listen-addresses, -// CRE_OCR_LISTEN_ADDRESSES). +// CRE_OCR_LISTEN_ADDRESSES) - the same namespace the delegating form uses. A binary hosts a peer or +// delegates to one, never both, so the names never meet, and an operator moving a deployment from +// one to the other keeps the settings it still has a use for. func (d *hostDependency) Namespace() string { return "ocr" } func (d *hostDependency) Config() any { return &d.cfg } @@ -87,11 +90,11 @@ func (d *hostDependency) Dependencies() []standalone.BootstrapCommand { // ForEmbedding returns the in-process form, which hosts no peer at all: an embedded instance's peers // are goroutines beside it, so there is nothing for a rage peer to listen on, announce to or // discover. None of this dependency's settings survive into it - see embedded. -func (d *hostDependency) ForEmbedding(i int) standalone.BootstrapDependency[*RageFactories] { +func (d *hostDependency) ForEmbedding(i, _ int) standalone.BootstrapDependency[*Factories] { return &embedded{lggr: d.lggr, index: i} } -func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (*RageFactories, error) { +func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Factories, error) { if len(d.cfg.ListenAddresses) == 0 { return nil, errors.New("--ocr.listen-addresses is required to host a peer") } @@ -113,15 +116,10 @@ func (d *hostDependency) Get(ctx context.Context, cc standalone.CommonConfig) (* if err != nil { return nil, err } + // The bundle itself rather than keyrings over it: this process signs on behalf of oracles + // elsewhere (see the Signer service), and an oracle here would build its own keyrings from the + // same bundle - see ocr2key.NewOCR3Keyring, which is what the delegating side ends up with. factories.OCR2 = bundle - // Held here, so an oracle in this process signs with the same bundle this serves to oracles - // elsewhere rather than going out over gRPC to reach a key it already has. - onchain, err := ocr2key.NewOCR3Keyring(evmFamily, bundle) - if err != nil { - return nil, err - } - // The bundle is already an offchain keyring, so only the onchain half is adapted. - factories.Keyrings = Keyrings{Offchain: bundle, Onchain: onchain} return factories, nil } @@ -150,7 +148,7 @@ func (d *hostDependency) keystoreIdentity(ctx context.Context, cc standalone.Com } // localFactories builds a real libocr peer and exposes its factories. -func (d *hostDependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*RageFactories, error) { +func (d *hostDependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyring, peerID ragetypes.PeerID) (*Factories, error) { discovererDB := ocrcommon.NewDiscovererDatabase(ds, peerID.String(), d.discovererTable) d.lggr.Infow("Creating local p2p peer", @@ -178,13 +176,13 @@ func (d *hostDependency) localFactories(ds *sqlx.DB, keyring ragetypes.PeerKeyri return nil, fmt.Errorf("failed to create rage peer: %w", err) } - return &RageFactories{ - OCRFactories: OCRFactories{ - OCR2Endpoint: peer.OCR2BinaryNetworkEndpointFactory(), - OCR3_1Endpoint: peer.OCR3_1BinaryNetworkEndpointFactory(), - PeerID: peerID, - closer: peer, - }, + return &Factories{ + Factories: ocr.NewFactories( + peer.OCR2BinaryNetworkEndpointFactory(), + peer.OCR3_1BinaryNetworkEndpointFactory(), + peerID, + peer, + ), PeerGroup: peer.PeerGroupFactory(), Keyring: keyring, }, nil diff --git a/libs/standalone/ocr/host_test.go b/libs/standalone/rage/host_test.go similarity index 78% rename from libs/standalone/ocr/host_test.go rename to libs/standalone/rage/host_test.go index 707493ca8..229dc0fab 100644 --- a/libs/standalone/ocr/host_test.go +++ b/libs/standalone/rage/host_test.go @@ -1,4 +1,4 @@ -package ocr +package rage import ( "testing" @@ -6,9 +6,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" ) func TestHostForEmbedding(t *testing.T) { @@ -16,8 +20,8 @@ func TestHostForEmbedding(t *testing.T) { // under test - an embedded instance neither unlocks a keystore nor stores announcements. template := Host(logger.Test(t), nil, "announcements") - first := template.ForEmbedding(0) - second := template.ForEmbedding(1) + first := template.ForEmbedding(0, 2) + second := template.ForEmbedding(1, 2) t.Run("each instance derives its own identity", func(t *testing.T) { ctx := t.Context() @@ -43,3 +47,12 @@ func TestHostForEmbedding(t *testing.T) { require.ErrorContains(t, err, "--ocr.listen-addresses is required") }) } + +// mustPeerID is the peer ID instance i derives, for asserting an embedded form resolved the right +// identity. +func mustPeerID(t *testing.T, i int) ragetypes.PeerID { + t.Helper() + peerID, err := ocr.DeterministicPeerID(i) + require.NoError(t, err) + return peerID +} diff --git a/libs/standalone/rage/keystore.go b/libs/standalone/rage/keystore.go new file mode 100644 index 000000000..c0ece2159 --- /dev/null +++ b/libs/standalone/rage/keystore.go @@ -0,0 +1,98 @@ +package rage + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + "github.com/jmoiron/sqlx" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/models" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +// This file is the node keystore, read: the one thing only a process hosting the node's peer does. +// +// It is here rather than beside the delegating form because of what it costs. The key ring holds a +// key of every kind the node has - cosmos, solana, starknet, TON, stellar - so reading it drags all +// of their libraries in, and a process that merely drives an oracle over someone else's peer has no +// use for any of it. Keeping it in this package means only a binary that hosts a peer pays. + +// loadPeerKeyring loads the P2P key from the node's keystore so this process +// uses the SAME peer identity as the node it fronts (other DON members expect +// this node's peer ID at this address). It reads the node's existing encrypted +// key ring (the legacy `encrypted_key_rings` table, in chainlink-common's +// corekeys/models format) and decrypts it with the keystore password. This is a +// deliberately small copy of core's keyManager.Unlock using only +// chainlink-common packages, so this binary needn't import chainlink core. +// +// password is the node's keystore password: this process shares the node's +// database and therefore its keystore password. +// +// TODO: drop this once the keystore is migrated to chainlink-common's +// keystore.Keystore + pgstore (as chainlink-ccv already uses), after which we +// can LoadKeystore from the shared table directly. +func loadPeerKeyring(ctx context.Context, ds *sqlx.DB, password string) (ragetypes.PeerKeyring, error) { + var encrypted []byte + if err := ds.GetContext(ctx, &encrypted, "SELECT encrypted_keys FROM encrypted_key_rings LIMIT 1"); err != nil { + return nil, fmt.Errorf("failed to read node key ring: %w", err) + } + kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(password) + if err != nil { + return nil, fmt.Errorf("failed to decrypt node key ring: %w", err) + } + for _, k := range kr.P2P { + return ocr.NewPeerKeyring(k) + } + return nil, errors.New("no P2P key found in node key ring") +} + +// loadOCR2Bundle loads the node's OCR2 key bundle from the same key ring the P2P +// key came from, so the oracle signing done on a capability's behalf is done +// with the node's own OCR identity - the one the registry lists as a signer. +// +// bundleID names which bundle when the node has several. A node running one +// capability DON usually has one, and naming it then would be one more thing to +// keep in step with the keystore, so an empty bundleID takes the only one there +// is and refuses to guess between several. +func loadOCR2Bundle(ctx context.Context, ds *sqlx.DB, password, bundleID string) (ocr2key.KeyBundle, error) { + var encrypted []byte + if err := ds.GetContext(ctx, &encrypted, "SELECT encrypted_keys FROM encrypted_key_rings LIMIT 1"); err != nil { + return nil, fmt.Errorf("failed to read node key ring: %w", err) + } + kr, err := models.EncryptedKeyRing{EncryptedKeys: encrypted}.Decrypt(password) + if err != nil { + return nil, fmt.Errorf("failed to decrypt node key ring: %w", err) + } + + if bundleID != "" { + bundle, ok := kr.OCR2[bundleID] + if !ok { + return nil, fmt.Errorf("no OCR2 key bundle %q in the node key ring", bundleID) + } + return bundle, nil + } + + switch len(kr.OCR2) { + case 0: + return nil, errors.New("no OCR2 key bundle found in the node key ring") + case 1: + for _, bundle := range kr.OCR2 { + return bundle, nil + } + } + + ids := make([]string, 0, len(kr.OCR2)) + for id := range kr.OCR2 { + ids = append(ids, id) + } + sort.Strings(ids) + return nil, fmt.Errorf("the node key ring holds %d OCR2 key bundles (%s), so --ocr.key-bundle-id must say which one to sign with", + len(ids), strings.Join(ids, ", ")) +} diff --git a/libs/x/registry/registry.go b/libs/x/registry/registry.go index ebbad81a8..b276f1359 100644 --- a/libs/x/registry/registry.go +++ b/libs/x/registry/registry.go @@ -13,7 +13,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" baseregistry "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types/core" ) @@ -37,7 +36,7 @@ type Handle struct { // // - Handles, registered at runtime over AddHandle by the processes that host the capabilities. // AddHandle dials the handle's address itself and wraps it into a real capabilities.BaseCapability -// (the same conversion registry/client does for its own callers), then adds that to local, +// (the same conversion the remote registry does for its own callers), then adds that to local, // chainlink's ordinary in-process base registry. AddHandle is a drop-in replacement for the // in-process Add(BaseCapability) core uses without a proxy: a capability registered this way is // just as real and callable as one added directly, whether the caller asking for it is in this @@ -132,7 +131,7 @@ func (r *Registry) AddHandle(ctx context.Context, h Handle) error { if err != nil { return fmt.Errorf("failed to dial capability %s at %s: %w", h.ID, h.URL, err) } - wrapped, err := client.Wrap(r.lggr, conn, h.Type) + wrapped, err := baseregistry.Wrap(r.lggr, conn, h.Type) if err != nil { _ = conn.Close() return fmt.Errorf("failed to wrap capability %s at %s: %w", h.ID, h.URL, err) diff --git a/libs/x/registry/server_test.go b/libs/x/registry/server_test.go index 0cc123dd2..8d986913d 100644 --- a/libs/x/registry/server_test.go +++ b/libs/x/registry/server_test.go @@ -19,7 +19,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/client" + baseregistry "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-protos/cre/go/values" @@ -75,16 +75,20 @@ func snapshot(t *testing.T, self ragetypes.PeerID, snap *registry.Snapshot) func func() (ragetypes.PeerID, error) { return self, nil }, snap)) } -// newClient serves reg and returns a client for it, with capability dials -// resolved through book when one is given. -func newClient(t *testing.T, reg *registry.Registry, book *registrytest.AddrBook) *client.Client { +// newClient serves reg and returns a registry client for it, with capability dials resolved through +// book when one is given. +// +// A client is a local registry with this one behind it, which is the only shape there is: announcing +// an address and holding a value are two halves of one registration. Nothing is added locally here, +// so what these tests exercise is the remote half. +func newClient(t *testing.T, reg *registry.Registry, book *registrytest.AddrBook) baseregistry.Registry { t.Helper() var opts []grpc.DialOption if book != nil { opts = append(opts, book.DialOption(), grpc.WithTransportCredentials(insecure.NewCredentials())) } - return client.New(logger.Test(t), registrytest.Serve(t, reg), opts...) + return baseregistry.Local(logger.Test(t)).WithRemote(registrytest.Serve(t, reg), nil, opts...) } // serveCapabilityAt registers impl in book under name and returns its address. @@ -93,7 +97,7 @@ func serveCapabilityAt(t *testing.T, book *registrytest.AddrBook, name string, t.Helper() srv := grpc.NewServer() - require.NoError(t, client.RegisterCapability(logger.Test(t), srv, impl, capType)) + require.NoError(t, baseregistry.RegisterCapability(logger.Test(t), srv, impl, capType)) return book.Serve(t, name, srv) } From 3044134d65c812cd92687cfc8cfe137f289b225c Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 20 Aug 2026 15:01:30 -0400 Subject: [PATCH 24/30] UI basics --- consensus/go.mod | 21 +- consensus/go.sum | 44 +- consensus/protos/consensus_server_gen.go | 11 + crecore/go.mod | 24 +- crecore/go.sum | 49 +- cron/go.mod | 21 +- cron/go.sum | 44 +- cron/protos/trigger_server_gen.go | 11 + libs/go.mod | 24 +- libs/go.sum | 49 +- libs/standalone/capability/capability.go | 14 + libs/standalone/capability/dependency.go | 71 ++ libs/standalone/capability/embed.go | 31 +- libs/standalone/ocr/embed_config.go | 23 +- libs/standalone/ocr/inproc.go | 19 +- libs/standalone/ocr/inproc_test.go | 11 +- .../protohelpers/protoc/server.go.tmpl | 12 + libs/standalone/protohelpers/ui/assets.go | 62 ++ libs/standalone/protohelpers/ui/context.go | 44 + libs/standalone/protohelpers/ui/errors.go | 102 +++ libs/standalone/protohelpers/ui/fleet.go | 99 +++ libs/standalone/protohelpers/ui/metadata.go | 445 ++++++++++ .../protohelpers/ui/metadata_test.go | 274 ++++++ libs/standalone/protohelpers/ui/mount.go | 356 ++++++++ .../protohelpers/ui/resources/form.js | 426 +++++++++ .../protohelpers/ui/resources/page.css | 283 ++++++ .../protohelpers/ui/resources/request.html | 91 ++ .../protohelpers/ui/resources/request.js | 814 ++++++++++++++++++ libs/standalone/protohelpers/ui/ui.go | 303 +++++++ libs/standalone/protohelpers/ui/ui_test.go | 535 ++++++++++++ 30 files changed, 4211 insertions(+), 102 deletions(-) create mode 100644 libs/standalone/protohelpers/ui/assets.go create mode 100644 libs/standalone/protohelpers/ui/context.go create mode 100644 libs/standalone/protohelpers/ui/errors.go create mode 100644 libs/standalone/protohelpers/ui/fleet.go create mode 100644 libs/standalone/protohelpers/ui/metadata.go create mode 100644 libs/standalone/protohelpers/ui/metadata_test.go create mode 100644 libs/standalone/protohelpers/ui/mount.go create mode 100644 libs/standalone/protohelpers/ui/resources/form.js create mode 100644 libs/standalone/protohelpers/ui/resources/page.css create mode 100644 libs/standalone/protohelpers/ui/resources/request.html create mode 100644 libs/standalone/protohelpers/ui/resources/request.js create mode 100644 libs/standalone/protohelpers/ui/ui.go create mode 100644 libs/standalone/protohelpers/ui/ui_test.go diff --git a/consensus/go.mod b/consensus/go.mod index 0b2796db0..2cabb757e 100644 --- a/consensus/go.mod +++ b/consensus/go.mod @@ -37,17 +37,23 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/consensys/gnark-crypto v0.20.1 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.9.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/ethereum/go-ethereum v1.17.3 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fullstorydev/grpcui v1.5.3 // indirect + github.com/fullstorydev/grpcurl v1.9.3 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -78,6 +84,8 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jhump/protoreflect v1.18.0 // indirect + github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -94,7 +102,9 @@ require ( github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect @@ -118,6 +128,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.16 // indirect @@ -151,11 +162,11 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect - golang.org/x/crypto v0.52.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect diff --git a/consensus/go.sum b/consensus/go.sum index ecb828599..3bd7cd8eb 100644 --- a/consensus/go.sum +++ b/consensus/go.sum @@ -44,6 +44,8 @@ github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny99 github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A= @@ -83,7 +85,11 @@ github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= @@ -99,6 +105,10 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fullstorydev/grpcui v1.5.3 h1:Rb4YYQ1fon0UY+nYZkTBk4rp5kKII94OuwdIR58TBPE= +github.com/fullstorydev/grpcui v1.5.3/go.mod h1:3siBzs0DsS/Q4qvFMdbweHHo53cXNm/BCarUU1wF/PA= +github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk= +github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -107,6 +117,8 @@ github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8x github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +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-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -231,8 +243,10 @@ 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/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= -github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jhump/protoreflect v1.18.0 h1:TOz0MSR/0JOZ5kECB/0ufGnC2jdsgZ123Rd/k4Z5/2w= +github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1 h1:Dw1rslK/VotaUGYsv53XVWITr+5RCPXfvvlGrM/+B6w= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= @@ -300,6 +314,8 @@ github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7ol github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= @@ -315,6 +331,8 @@ github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1 github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -384,6 +402,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -490,8 +510,8 @@ go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfP golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= @@ -510,16 +530,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -538,15 +558,15 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/consensus/protos/consensus_server_gen.go b/consensus/protos/consensus_server_gen.go index 5b26e5df2..7e3d714c4 100644 --- a/consensus/protos/consensus_server_gen.go +++ b/consensus/protos/consensus_server_gen.go @@ -10,6 +10,8 @@ import ( "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/reflect/protoreflect" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" ) @@ -78,6 +80,15 @@ var _ capabilities.ExecutableAndTriggerCapability = (*consensusCapability)(nil) const ConsensusID = "consensus@1.0.0-alpha" +// Service is the proto service this server was generated from. +// +// Taken from the file descriptor rather than rebuilt, so it is the same +// descriptor the messages were generated against: whatever reads it sees the +// methods, and their input and output types, exactly as the proto declares them. +func (c *consensusCapability) Service() protoreflect.ServiceDescriptor { + return File_capabilities_internal_consensus_v1alpha_consensus_proto.Services().ByName("Consensus") +} + func (c *consensusCapability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { return nil, fmt.Errorf("trigger %s not found", request.Method) } diff --git a/crecore/go.mod b/crecore/go.mod index b23e6d183..d673f57e3 100644 --- a/crecore/go.mod +++ b/crecore/go.mod @@ -51,6 +51,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect @@ -80,18 +81,23 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emicklei/dot v1.6.2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ethereum/go-ethereum v1.17.3 // indirect github.com/fatih/color v1.19.0 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fullstorydev/grpcui v1.5.3 // indirect + github.com/fullstorydev/grpcurl v1.9.3 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/binary v0.8.0 // indirect github.com/gagliardetto/solana-go v1.13.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -146,6 +152,8 @@ require ( github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jhump/protoreflect v1.18.0 // indirect + github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect @@ -176,13 +184,14 @@ require ( github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect - github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/stun/v2 v2.0.0 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pressly/goose/v3 v3.27.1 // indirect github.com/prometheus/client_golang v1.23.2 // indirect @@ -220,6 +229,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stellar/go-stellar-sdk v0.4.0 // indirect github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect @@ -271,13 +281,13 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect diff --git a/crecore/go.sum b/crecore/go.sum index b3ca061ef..cd98019d3 100644 --- a/crecore/go.sum +++ b/crecore/go.sum @@ -105,6 +105,8 @@ github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= @@ -192,7 +194,11 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= @@ -215,6 +221,10 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fullstorydev/grpcui v1.5.3 h1:Rb4YYQ1fon0UY+nYZkTBk4rp5kKII94OuwdIR58TBPE= +github.com/fullstorydev/grpcui v1.5.3/go.mod h1:3siBzs0DsS/Q4qvFMdbweHHo53cXNm/BCarUU1wF/PA= +github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk= +github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -232,6 +242,8 @@ github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvN github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +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-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -475,8 +487,10 @@ 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/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= -github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jhump/protoreflect v1.18.0 h1:TOz0MSR/0JOZ5kECB/0ufGnC2jdsgZ123Rd/k4Z5/2w= +github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1 h1:Dw1rslK/VotaUGYsv53XVWITr+5RCPXfvvlGrM/+B6w= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= @@ -613,8 +627,9 @@ github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7ol github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -639,6 +654,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -753,6 +770,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stellar/go-stellar-sdk v0.4.0 h1:WBLeJk7EllU7WhwrTH1L5Mu2EhZor/xy5sVi/82JzNQ= github.com/stellar/go-stellar-sdk v0.4.0/go.mod h1:tLKAQPxa2I5UvGMabBbUXcY3fmgYnfDudrMeK7CDX4w= github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf h1:GY1RVbX3Hg7poPXEf6yojjP0hyypvgUgZmCqQU9D0xg= @@ -941,8 +960,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= @@ -989,8 +1008,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1003,8 +1022,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1057,8 +1076,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1068,8 +1087,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1081,8 +1100,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/cron/go.mod b/cron/go.mod index 19095e9d6..475da40ab 100644 --- a/cron/go.mod +++ b/cron/go.mod @@ -28,12 +28,18 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dominikbraun/graph v0.23.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fullstorydev/grpcui v1.5.3 // indirect + github.com/fullstorydev/grpcurl v1.9.3 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -59,6 +65,8 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jhump/protoreflect v1.18.0 // indirect + github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -75,7 +83,9 @@ require ( github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -100,6 +110,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -126,12 +137,12 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect diff --git a/cron/go.sum b/cron/go.sum index dd8502ea3..a30077dba 100644 --- a/cron/go.sum +++ b/cron/go.sum @@ -28,6 +28,8 @@ github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny99 github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -38,7 +40,11 @@ github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= @@ -46,12 +52,18 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fullstorydev/grpcui v1.5.3 h1:Rb4YYQ1fon0UY+nYZkTBk4rp5kKII94OuwdIR58TBPE= +github.com/fullstorydev/grpcui v1.5.3/go.mod h1:3siBzs0DsS/Q4qvFMdbweHHo53cXNm/BCarUU1wF/PA= +github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk= +github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-co-op/gocron/v2 v2.18.0 h1:DS3Uhru66q1jy/5f9V0itmi3cLXcn2b7N+duGfgT7gU= github.com/go-co-op/gocron/v2 v2.18.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= +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-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -138,8 +150,10 @@ github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/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/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= -github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jhump/protoreflect v1.18.0 h1:TOz0MSR/0JOZ5kECB/0ufGnC2jdsgZ123Rd/k4Z5/2w= +github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1 h1:Dw1rslK/VotaUGYsv53XVWITr+5RCPXfvvlGrM/+B6w= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= @@ -195,10 +209,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -257,6 +275,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -343,8 +363,8 @@ go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfP golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= @@ -363,16 +383,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -389,15 +409,15 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/cron/protos/trigger_server_gen.go b/cron/protos/trigger_server_gen.go index 9e48d22d2..4f31bab0c 100644 --- a/cron/protos/trigger_server_gen.go +++ b/cron/protos/trigger_server_gen.go @@ -8,6 +8,8 @@ import ( "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/reflect/protoreflect" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" ) @@ -79,6 +81,15 @@ var _ capabilities.ExecutableAndTriggerCapability = (*cronCapability)(nil) const CronID = "cron-trigger@1.0.0" +// Service is the proto service this server was generated from. +// +// Taken from the file descriptor rather than rebuilt, so it is the same +// descriptor the messages were generated against: whatever reads it sees the +// methods, and their input and output types, exactly as the proto declares them. +func (c *cronCapability) Service() protoreflect.ServiceDescriptor { + return File_capabilities_scheduler_cron_v1_trigger_proto.Services().ByName("Cron") +} + func (c *cronCapability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { ctx = request.Metadata.ContextWithCRE(ctx) switch request.Method { diff --git a/libs/go.mod b/libs/go.mod index ce374b6e2..b7a000f93 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -5,10 +5,12 @@ go 1.26.2 require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/ethereum/go-ethereum v1.17.3 + github.com/fullstorydev/grpcui v1.5.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/grafana/pyroscope-go v1.3.0 github.com/hashicorp/go-plugin v1.8.0 + github.com/jhump/protoreflect v1.18.0 github.com/jmoiron/sqlx v1.4.0 github.com/mr-tron/base58 v1.3.0 github.com/pkg/errors v0.9.1 @@ -29,7 +31,7 @@ require ( go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.uber.org/zap v1.28.0 - golang.org/x/crypto v0.52.0 + golang.org/x/crypto v0.53.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 @@ -68,6 +70,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect @@ -94,15 +97,19 @@ require ( github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fullstorydev/grpcurl v1.9.3 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/binary v0.8.0 // indirect github.com/gagliardetto/solana-go v1.13.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -144,6 +151,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect @@ -170,7 +178,8 @@ require ( github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect - github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect @@ -197,6 +206,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stellar/go-stellar-sdk v0.4.0 // indirect github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect @@ -242,11 +252,11 @@ require ( go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect diff --git a/libs/go.sum b/libs/go.sum index b2af0106e..508841fd6 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -103,6 +103,8 @@ github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= @@ -188,7 +190,11 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= @@ -211,6 +217,10 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fullstorydev/grpcui v1.5.3 h1:Rb4YYQ1fon0UY+nYZkTBk4rp5kKII94OuwdIR58TBPE= +github.com/fullstorydev/grpcui v1.5.3/go.mod h1:3siBzs0DsS/Q4qvFMdbweHHo53cXNm/BCarUU1wF/PA= +github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk= +github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -228,6 +238,8 @@ github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvN github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +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-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -420,8 +432,10 @@ 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/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= -github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jhump/protoreflect v1.18.0 h1:TOz0MSR/0JOZ5kECB/0ufGnC2jdsgZ123Rd/k4Z5/2w= +github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1 h1:Dw1rslK/VotaUGYsv53XVWITr+5RCPXfvvlGrM/+B6w= +github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= @@ -547,8 +561,9 @@ github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7ol github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -569,6 +584,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -667,6 +684,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stellar/go-stellar-sdk v0.4.0 h1:WBLeJk7EllU7WhwrTH1L5Mu2EhZor/xy5sVi/82JzNQ= github.com/stellar/go-stellar-sdk v0.4.0/go.mod h1:tLKAQPxa2I5UvGMabBbUXcY3fmgYnfDudrMeK7CDX4w= github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf h1:GY1RVbX3Hg7poPXEf6yojjP0hyypvgUgZmCqQU9D0xg= @@ -821,8 +840,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= @@ -859,8 +878,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -871,8 +890,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -914,19 +933,19 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/libs/standalone/capability/capability.go b/libs/standalone/capability/capability.go index c2866531e..332b2443b 100644 --- a/libs/standalone/capability/capability.go +++ b/libs/standalone/capability/capability.go @@ -1,6 +1,8 @@ package capability import ( + "google.golang.org/protobuf/reflect/protoreflect" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/services" ) @@ -17,4 +19,16 @@ import ( type Capability interface { services.Service capabilities.ExecutableAndTriggerCapability + + // Service is the proto service this capability's server was generated from. + // + // The untyped capability API says nothing about the methods behind it: a + // request carries a method name and an opaque payload, and what shapes that + // payload is the proto the server was generated from. Handing the descriptor + // back is what lets something outside the capability - the debug UI, say - + // know which methods exist and what each one takes, without the capability + // having to describe itself twice. + // + // The generated server implements this, so a capability gets it for free. + Service() protoreflect.ServiceDescriptor } diff --git a/libs/standalone/capability/dependency.go b/libs/standalone/capability/dependency.go index 7ac9b14f2..50a98f844 100644 --- a/libs/standalone/capability/dependency.go +++ b/libs/standalone/capability/dependency.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/capabilities/libs/standalone" standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" + "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/ui" "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" @@ -67,6 +68,17 @@ type Dependencies struct { servers *standalonegrpc.Factory // closers are the connections Get opened, torn down by Close. closers []func() error + + // httpDebug says whether Run mounts the debug UI. + httpDebug bool + // index is this instance's number, 0 for a configured run and i for instance i + // of an embed run. It names the instance on the fan-out page. + index int + // fleet is every instance's debug page, shared by pointer between the + // configured dependency and each embedded form - the same way the embedded + // config is - so the fan-out page can reach a sibling by calling into its + // handler rather than over a socket. nil when the UI is off. + fleet *ui.Fleet } // Close releases whatever Get dialled. The bootstrapper closes resolved @@ -88,6 +100,15 @@ type capabilityConfig struct { // a different target - and none of them can be expressed as a port. ProxyURL string `validate:"required" usage:"gRPC target of the node's capability registry proxy (e.g. localhost:9000, or dns:///registry.internal:9000), used to resolve capabilities this binary does not host"` CapabilityDonID uint32 `validate:"required" usage:"on-chain DON ID of the capability DON this process was spawned for"` + + // HTTPDebug serves the debug UI on the shared HTTP server. Off by default: it + // invokes capabilities, so it is something a run opts into rather than + // something a configured process exposes because it can. + // + // An embed run has it on regardless - see embedded.Get. Embedding is the local + // shape of this binary, run to be poked at, and a flag to turn on the thing you + // started it for is a flag nobody wants to remember. + HTTPDebug bool `usage:"serve the capability debug UI on the shared HTTP server, under /debug/capabilities. Always on for an embed run"` } // Dependency returns the standalone.BootstrapDependency a capability binary @@ -111,6 +132,7 @@ func Dependency(lggr logger.Logger, servers common.BootstrapDependency[*standalo lggr: lggr, servers: servers, embeddedConfig: &embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}, + fleet: &ui.Fleet{}, }) } @@ -127,6 +149,11 @@ type dependency struct { // each instance are built later (see ForEmbedding). A config per form would leave the decoded // values in the first one and every instance reading the defaults. embeddedConfig *embeddedConfig + + // fleet is created once and shared with every embedded form, for the same reason + // the config is: all of an embed run's instances register their debug page in + // one list, so the fan-out page on any of them can reach the rest. + fleet *ui.Fleet } var _ common.BootstrapDependency[Dependencies] = (*dependency)(nil) @@ -157,6 +184,8 @@ func (d *dependency) ForEmbedding(i, instances int) common.BootstrapDependency[D servers: d.servers.ForEmbedding(i, instances), instances: instances, cfg: d.embeddedConfig, + fleet: d.fleet, + index: i, } } @@ -198,6 +227,8 @@ func (d *dependency) Get(ctx context.Context, cc common.CommonConfig) (Dependenc addresses: addresses, servers: servers, closers: []func() error{proxy.Close, conn.Close}, + httpDebug: d.HTTPDebug, + fleet: d.fleet, }, nil } @@ -251,6 +282,12 @@ func Run(dependencies Dependencies, sc standalone.StandaloneConfig, caps ...Capa // routes: it starts serving the mux only once every service has started. sc.Mux.HandleFunc(ReloadPath(), reloadHandler(sc.Logger, dependencies.settings, dependencies.settingsPath)) + if dependencies.httpDebug { + if err := mountDebugUI(sc, dependencies, caps); err != nil { + return nil, err + } + } + svcs := make([]services.Service, 0, len(caps)+1) for _, c := range caps { svcs = append(svcs, c) @@ -259,6 +296,40 @@ func Run(dependencies Dependencies, sc standalone.StandaloneConfig, caps ...Capa return svcs, nil } +// mountDebugUI serves the debug page for the capabilities this instance hosts. +// +// The page calls capabilities through the registry, as any caller would, so what +// it exercises is the path a workflow takes rather than a way around it. Every +// instance mounts its own, and each adds itself to the shared fleet, which is what +// lets the fan-out page on any of them reach the rest. +// +// A context is needed to read each capability's ID, and Run has none to give: the +// bootstrapper builds services before starting them. context.Background is right +// here because this only reads what the capability already knows - it does not +// start anything, and there is nothing for a cancelled boot to abandon. +func mountDebugUI(sc standalone.StandaloneConfig, dependencies Dependencies, caps []Capability) error { + // Widened to what the UI asks for, which is less than a Capability: it only + // reads what a capability is registered as and the service behind it. + debuggable := make([]ui.Capability, 0, len(caps)) + for _, c := range caps { + debuggable = append(debuggable, c) + } + + server, err := ui.New(context.Background(), dependencies.CapabilityRegistry, debuggable...) + if err != nil { + return fmt.Errorf("failed to build the capability debug UI: %w", err) + } + + title := fmt.Sprintf("Capability debug (instance %d)", dependencies.index+1) + if err := ui.Mount(sc.Mux, ui.DefaultPrefix, server, dependencies.fleet, dependencies.index, title); err != nil { + return fmt.Errorf("failed to mount the capability debug UI: %w", err) + } + + sc.Logger.Infow("Serving the capability debug UI", + "path", ui.DefaultPrefix+"/ui/", "fanout", ui.DefaultPrefix+"/request") + return nil +} + // reloadHandler re-reads the settings file and swaps it in. 200 means every limit // in this process now resolves against the new settings; 500 means none of them // do and the previous settings are still in force. diff --git a/libs/standalone/capability/embed.go b/libs/standalone/capability/embed.go index 31792e030..62eaa31e5 100644 --- a/libs/standalone/capability/embed.go +++ b/libs/standalone/capability/embed.go @@ -5,6 +5,7 @@ import ( "fmt" standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" + "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/ui" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" @@ -48,6 +49,13 @@ type embedded struct { // every other instance's form, so all of them read the settings the flags were // bound to rather than a copy of the defaults. cfg *embeddedConfig + + // fleet is shared the same way, and for the same reason: every instance's debug + // page goes into one list, so the fan-out page can reach a sibling by calling + // into its handler. + fleet *ui.Fleet + // index is this instance's number, which names it on the fan-out page. + index int } var _ common.BootstrapDependency[Dependencies] = (*embedded)(nil) @@ -69,7 +77,14 @@ func (d *embedded) Dependencies() []common.BootstrapCommand { // instance i is its own business, and this only has to ask it the same question // the configured form did. func (d *embedded) ForEmbedding(i, instances int) common.BootstrapDependency[Dependencies] { - return &embedded{lggr: d.lggr, servers: d.servers.ForEmbedding(i, instances), instances: instances, cfg: d.cfg} + return &embedded{ + lggr: d.lggr, + servers: d.servers.ForEmbedding(i, instances), + instances: instances, + cfg: d.cfg, + fleet: d.fleet, + index: i, + } } func (d *embedded) Get(ctx context.Context, cc common.CommonConfig) (Dependencies, error) { @@ -104,9 +119,15 @@ func (d *embedded) Get(ctx context.Context, cc common.CommonConfig) (Dependencie // have arrived on either way - see RegisterEmbeddedOCRConfig. OCRConfigRegistry: embeddedOCRConfigRegistry(d.instances), CapabilityDonID: cfg.CapabilityDonID, - lggr: d.lggr, - settings: settings, - settingsPath: SettingsPath(), - servers: servers, + // Always on when embedded: an embed run is this binary run to be poked at, + // so a flag to enable the thing you started it for would only ever be + // forgotten. + httpDebug: true, + fleet: d.fleet, + index: d.index, + lggr: d.lggr, + settings: settings, + settingsPath: SettingsPath(), + servers: servers, }, nil } diff --git a/libs/standalone/ocr/embed_config.go b/libs/standalone/ocr/embed_config.go index b444a4c12..760aa0bff 100644 --- a/libs/standalone/ocr/embed_config.go +++ b/libs/standalone/ocr/embed_config.go @@ -60,24 +60,33 @@ const ( // defaults to the largest value the instance count allows (see EmbeddedOCRConfig). // // The values are brisk rather than production-safe: an embed run is watched by whoever started it, -// and a round every couple of seconds is what makes it worth watching. +// and a round a second is what makes it worth watching. They are still consistent with each other, +// which matters more than any one of them: DeltaProgress has to exceed what a round can take - a +// round, plus the four MaxDurations a plugin may spend in it - or the progress timer can fire on a +// leader that was doing nothing wrong and move the epoch on under it. // // Package-level because every instance runs the same protocol - they are one DON - and because what // changes it is a flag rather than a caller. var DefaultEmbeddedOCRConfig = ocr3confighelper.PublicConfig{ - DeltaProgress: 2 * time.Second, + DeltaProgress: 5 * time.Second, DeltaResend: 2 * time.Second, DeltaInitial: 500 * time.Millisecond, DeltaRound: time.Second, DeltaGrace: 500 * time.Millisecond, DeltaCertifiedCommitRequest: 500 * time.Millisecond, DeltaStage: 5 * time.Second, - RMax: 5, + // Rounds per epoch, after which the leader rotates. Rotation is normal - libocr says so with + // "epoch has been going on for too long" - so this is only how much of it a reader of the log + // sees: low enough that rotation happens while someone is watching, high enough that most of + // what they see is rounds. + RMax: 20, - MaxDurationQuery: time.Second, - MaxDurationObservation: time.Second, - MaxDurationShouldAcceptAttestedReport: time.Second, - MaxDurationShouldTransmitAcceptedReport: time.Second, + // What a plugin may spend in one round. Generous for a capability whose observation is a batch of + // pending requests held in memory, and together well inside DeltaProgress. + MaxDurationQuery: 300 * time.Millisecond, + MaxDurationObservation: 300 * time.Millisecond, + MaxDurationShouldAcceptAttestedReport: 300 * time.Millisecond, + MaxDurationShouldTransmitAcceptedReport: 300 * time.Millisecond, } // init hands the builder below to the capability dependency, which is where an OCR-based capability diff --git a/libs/standalone/ocr/inproc.go b/libs/standalone/ocr/inproc.go index 03e0c832e..7de586db6 100644 --- a/libs/standalone/ocr/inproc.go +++ b/libs/standalone/ocr/inproc.go @@ -25,7 +25,8 @@ import ( // send side is a channel, and enforcing a byte budget on it would only slow down a test), the // bootstrapper locators (there are no addresses to dial), and delivery to peers that have not // created their endpoint yet or whose mailbox is full (dropped, as ragep2p drops when a peer's -// buffer is full). Payloads are copied on send, so a caller reusing its buffer cannot corrupt a +// buffer is full). A message to the sender itself is not among them: libocr's own endpoints deliver +// those, and the protocol needs them. Payloads are copied on send, so a caller reusing its buffer cannot corrupt a // message in flight the way it could not over a real socket. // network is the in-process transport shared by every instance in the process: a registry of @@ -143,11 +144,16 @@ func (e *ocr2Endpoint) SendTo(payload []byte, to commontypes.OracleID) { e.deliver(payload, e.peerIDs[to]) } +// Broadcast delivers to every oracle of this configuration, this one included. +// +// Including itself is not a detail: a libocr endpoint loops a broadcast back to its own receive +// channel (see ocrEndpointV2.SendTo's sendToSelf), and the protocol relies on it - a leader learns +// what round it started by receiving its own round-start like anyone else. An endpoint that skipped +// itself would leave a leader broadcasting rounds it never enters, dropping every observation those +// rounds produced as having the wrong sequence number, until the progress timer moved the epoch on +// and it happened again. func (e *ocr2Endpoint) Broadcast(payload []byte) { for _, peerID := range e.peerIDs { - if peerID == e.key.peerID { - continue - } e.deliver(payload, peerID) } } @@ -256,11 +262,10 @@ func (e *binaryNetworkEndpoint2) SendTo(msg ocr2types.OutboundBinaryMessage, to e.deliver(msg, e.peerIDs[to]) } +// Broadcast delivers to every oracle of this configuration, this one included - see +// ocr2Endpoint.Broadcast for why the sender is not skipped. func (e *binaryNetworkEndpoint2) Broadcast(msg ocr2types.OutboundBinaryMessage) { for _, peerID := range e.peerIDs { - if peerID == e.key.peerID { - continue - } e.deliver(msg, peerID) } } diff --git a/libs/standalone/ocr/inproc_test.go b/libs/standalone/ocr/inproc_test.go index 0847d8c97..d8778db30 100644 --- a/libs/standalone/ocr/inproc_test.go +++ b/libs/standalone/ocr/inproc_test.go @@ -55,15 +55,16 @@ func TestInprocOCR2Endpoints(t *testing.T) { assertNothingReceived(t, endpoints[1].Receive()) }) - t.Run("Broadcast reaches every other oracle but not the sender", func(t *testing.T) { + // The sender included: libocr's own endpoints loop a broadcast back to the sender, and the + // protocol needs it - a leader enters the round it started by receiving its own round-start. + t.Run("Broadcast reaches every oracle, the sender included", func(t *testing.T) { endpoints[0].Broadcast([]byte("to everyone")) - for _, i := range []int{1, 2} { + for _, i := range []int{0, 1, 2} { msg := receive(t, endpoints[i].Receive()) - assert.Equal(t, []byte("to everyone"), msg.Msg) - assert.Equal(t, commontypes.OracleID(0), msg.Sender) + assert.Equal(t, []byte("to everyone"), msg.Msg, "oracle %d", i) + assert.Equal(t, commontypes.OracleID(0), msg.Sender, "oracle %d", i) } - assertNothingReceived(t, endpoints[0].Receive()) }) t.Run("payloads are copied, so a reused buffer cannot rewrite a sent message", func(t *testing.T) { diff --git a/libs/standalone/protohelpers/protoc/server.go.tmpl b/libs/standalone/protohelpers/protoc/server.go.tmpl index ec931f825..d757fdc2b 100644 --- a/libs/standalone/protohelpers/protoc/server.go.tmpl +++ b/libs/standalone/protohelpers/protoc/server.go.tmpl @@ -18,6 +18,8 @@ import ( {{.}} {{- end }} + "google.golang.org/protobuf/reflect/protoreflect" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" ) @@ -25,6 +27,7 @@ import ( // Avoid unused imports if there is configuration type var _ = emptypb.Empty{} +{{- $file := . }} {{- range .Services}} {{ $hasTriggers := false }} {{ $hasActions := false }} @@ -107,6 +110,15 @@ var _ capabilities.ExecutableAndTriggerCapability = (*{{.GoName|LowerFirst}}Capa const {{.GoName}}ID = "{{CapabilityId $service}}" +// Service is the proto service this server was generated from. +// +// Taken from the file descriptor rather than rebuilt, so it is the same +// descriptor the messages were generated against: whatever reads it sees the +// methods, and their input and output types, exactly as the proto declares them. +func (c *{{.GoName|LowerFirst}}Capability) Service() protoreflect.ServiceDescriptor { + return {{$file.GoDescriptorIdent.GoName}}.Services().ByName("{{$service.Desc.Name}}") +} + func (c *{{.GoName|LowerFirst}}Capability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { {{- if $hasTriggers }} ctx = request.Metadata.ContextWithCRE(ctx) diff --git a/libs/standalone/protohelpers/ui/assets.go b/libs/standalone/protohelpers/ui/assets.go new file mode 100644 index 000000000..bbe482a9e --- /dev/null +++ b/libs/standalone/protohelpers/ui/assets.go @@ -0,0 +1,62 @@ +package ui + +import ( + "crypto/sha256" + _ "embed" + "encoding/hex" + "encoding/json" + "fmt" +) + +// The page's own stylesheet and scripts. Both pages share the stylesheet, and the +// form page's script is served from grpcui's asset route while the fan-out page's +// is served from ours. +var ( + //go:embed resources/page.css + pageCSS string + + //go:embed resources/form.js + formJS string + + //go:embed resources/request.js + requestJS string + + //go:embed resources/request.html + requestHTML string +) + +// Assets are served under content-hashed names. +// +// grpcui serves what it is given with "Cache-Control: private, max-age=3600" and +// only revalidates its index, so at a fixed name a rebuilt binary would not reach +// an already-open browser for an hour - which looks exactly like the page being +// broken. A name that changes with the content cannot be served stale. +func hashedName(base, content, ext string) string { + sum := sha256.Sum256([]byte(content)) + return fmt.Sprintf("%s.%s.%s", base, hex.EncodeToString(sum[:])[:12], ext) +} + +func cssFileName() string { return hashedName("cre-debug", pageCSS, "css") } +func jsFileName(served string) string { return hashedName("cre-debug", served, "js") } +func requestJSFileName() string { return hashedName("cre-debug-request", requestJS, "js") } + +// customJS is form.js with the page's configuration prepended, so the browser has +// it without a second request. +// +// The configuration is what keeps the page from guessing: the metadata fields come +// from the RequestMetadata type, and the methods from the descriptors the +// capabilities were generated against. +func customJS(s *Server) (string, error) { + cfg := struct { + Metadata []Field `json:"metadata"` + Prefix string `json:"headerPrefix"` + }{ + Metadata: Fields(), + Prefix: HeaderPrefix, + } + encoded, err := json.Marshal(cfg) + if err != nil { + return "", fmt.Errorf("encoding the debug page config: %w", err) + } + return "window.__CRE_DEBUG__ = " + string(encoded) + ";\n" + formJS, nil +} diff --git a/libs/standalone/protohelpers/ui/context.go b/libs/standalone/protohelpers/ui/context.go new file mode 100644 index 000000000..f2dcd5ef5 --- /dev/null +++ b/libs/standalone/protohelpers/ui/context.go @@ -0,0 +1,44 @@ +package ui + +import ( + "context" + "net/http" + + "google.golang.org/grpc/metadata" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" +) + +// metadataFromContext reads the request metadata off the gRPC metadata the form +// generator forwarded from the browser's headers. +// +// The headers reach here as outgoing metadata, keyed lowercase, which is why the +// lookup lowercases rather than relying on the caller's casing. +func metadataFromContext(ctx context.Context) (capabilities.RequestMetadata, error) { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.MD{} + } + return MetadataFromHeaders(func(name string) []string { + return md.Get(name) + }) +} + +// MetadataFromRequest is the same for an ordinary HTTP request, which is what the +// fan-out endpoint has: it holds headers rather than gRPC metadata. +func MetadataFromRequest(r *http.Request) (capabilities.RequestMetadata, error) { + return MetadataFromHeaders(func(name string) []string { + return r.Header.Values(name) + }) +} + +// HeaderNames is every header the metadata travels in, which is what the form +// generator has to be told to forward. +func HeaderNames() []string { + fields := Fields() + names := make([]string, 0, len(fields)) + for _, f := range fields { + names = append(names, f.Header) + } + return names +} diff --git a/libs/standalone/protohelpers/ui/errors.go b/libs/standalone/protohelpers/ui/errors.go new file mode 100644 index 000000000..9391454c0 --- /dev/null +++ b/libs/standalone/protohelpers/ui/errors.go @@ -0,0 +1,102 @@ +package ui + +import ( + "errors" + "fmt" + "net/http" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" +) + +// The page tells the two kinds of failure apart, because they are not the same +// news: something the user typed is theirs to correct, and something this could +// not do is ours. +// +// The distinction is carried as a gRPC status, which is also what decides how a +// failure reaches the browser. grpcurl returns a plain error from an Invoke to +// grpcui as an infrastructure failure, which grpcui answers with a 500 - so a +// mistyped number would be reported as the server having broken. A status error +// instead comes back as a failed RPC, rendered in the Response tab where the user +// can see what they did and fix it. +// +// So: user errors are statuses, system errors are not, and 500 is reserved for the +// second kind. + +// userErrorf is a failure the caller can fix: a value that will not parse, a +// method that does not exist, a request that is not the shape the method takes. +func userErrorf(format string, args ...any) error { + return status.Errorf(codes.InvalidArgument, format, args...) +} + +// systemErrorf is a failure the caller cannot do anything about. Left as a plain +// error so it surfaces as one rather than as something the user got wrong. +func systemErrorf(format string, args ...any) error { + return fmt.Errorf(format, args...) +} + +// fromCapability classifies what a capability answered with. +// +// A capability that failed is not the page failing: it was asked something and it +// answered. So neither outcome is a 500 from this package's point of view - but a +// capability already says whose fault it was, via Origin, and repeating that is +// better than flattening it. OriginUser becomes a user error, so the browser is +// told what to change; anything else is reported as the capability's own failure. +func fromCapability(err error) error { + if err == nil { + return nil + } + + var capErr caperrors.Error + if errors.As(err, &capErr) { + if capErr.Origin() == caperrors.OriginUser { + return status.Error(codes.InvalidArgument, capErr.Error()) + } + return status.Error(codes.FailedPrecondition, capErr.Error()) + } + + // No origin to go on. Reported as a failed call rather than a broken page, + // because the call is what failed: the request reached a capability and came + // back with this. + return status.Error(codes.Unknown, err.Error()) +} + +// isUserError reports whether err is one of ours from userErrorf. +// +// Only InvalidArgument counts. A capability answering InvalidArgument of its own +// accord is saying the same thing about the same request, so treating it the same +// way is right rather than convenient. +func isUserError(err error) bool { + if err == nil { + return false + } + var se interface{ GRPCStatus() *status.Status } + if errors.As(err, &se) { + return se.GRPCStatus().Code() == codes.InvalidArgument + } + return false +} + +// httpStatus is the code a failure is answered with: the caller's mistake is a +// 400, and anything else is a 500 - which is what makes a 500 mean the page +// itself failed rather than that the request was wrong. +func httpStatus(err error) int { + if isUserError(err) { + return http.StatusBadRequest + } + return http.StatusInternalServerError +} + +// writeError answers a request with the failure and the code that fits it. +func writeError(w http.ResponseWriter, err error) { + message := err.Error() + if s, ok := status.FromError(err); ok { + // Without this the body reads "rpc error: code = InvalidArgument desc = + // ...", which is the transport talking rather than the thing that went + // wrong. + message = s.Message() + } + http.Error(w, message, httpStatus(err)) +} diff --git a/libs/standalone/protohelpers/ui/fleet.go b/libs/standalone/protohelpers/ui/fleet.go new file mode 100644 index 000000000..481baf972 --- /dev/null +++ b/libs/standalone/protohelpers/ui/fleet.go @@ -0,0 +1,99 @@ +package ui + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "sync" +) + +// Instance is one instance's debug page, as the fan-out reaches it. +// +// It holds the instance's handler rather than its address. Instances of an embed +// run share a process, so a request to a sibling is a call into its handler: no +// port to work out, no socket, and nothing to go wrong between them. It also +// means the fan-out works when the gRPC servers were given port 0 and there is no +// arithmetic that could have found them. +type Instance struct { + Index int `json:"index"` + Label string `json:"label"` + + handler http.Handler +} + +// Fleet is every instance's page, shared by pointer between the configured +// dependency and the embedded form each instance resolves - the same way the +// embedded config is shared - so each instance adds itself to one list and any of +// them can reach the rest. +// +// Instances are constructed one after another and register during construction, +// so the list is complete before the process is serving. The mutex is for that +// construction, not for the requests that read it afterwards. +type Fleet struct { + mu sync.Mutex + instances []*Instance +} + +// Add registers an instance's page. Called once per instance, during construction. +func (f *Fleet) Add(in *Instance) { + f.mu.Lock() + defer f.mu.Unlock() + f.instances = append(f.instances, in) +} + +// List is every registered instance, in the order they were added, which is +// instance order. +func (f *Fleet) List() []*Instance { + f.mu.Lock() + defer f.mu.Unlock() + out := make([]*Instance, len(f.instances)) + copy(out, f.instances) + return out +} + +// invoke calls one method on this instance and returns what its page answered. +// +// The call goes through the instance's own handler, so it takes exactly the path a +// browser's request would: the same decoding of the posted JSON into the method's +// message, the same CSRF check, the same wrapping into a CapabilityRequest. Only +// the transport is skipped. +func (in *Instance) invoke(method string, body []byte, header http.Header) (json.RawMessage, error) { + // The index page is what issues the CSRF cookie, exactly as it would over a + // socket. Asking for it here keeps the handler's own check meaningful rather + // than working around it. + index := httptest.NewRecorder() + in.handler.ServeHTTP(index, httptest.NewRequest(http.MethodGet, "/", nil)) + + token := "" + for _, c := range index.Result().Cookies() { + if c.Name == csrfCookieName { + token = c.Value + break + } + } + if token == "" { + return nil, fmt.Errorf("instance %d did not issue a %s cookie", in.Index, csrfCookieName) + } + + req := httptest.NewRequest(http.MethodPost, "/invoke/"+method, bytes.NewReader(body)) + for name, values := range header { + for _, v := range values { + req.Header.Add(name, v) + } + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set(csrfHeaderName, token) + req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: token}) + + rec := httptest.NewRecorder() + in.handler.ServeHTTP(rec, req) + + result := rec.Result() + defer result.Body.Close() + if result.StatusCode != http.StatusOK { + return nil, fmt.Errorf("instance %d returned %d: %s", in.Index, result.StatusCode, bytes.TrimSpace(rec.Body.Bytes())) + } + return json.RawMessage(rec.Body.Bytes()), nil +} diff --git a/libs/standalone/protohelpers/ui/metadata.go b/libs/standalone/protohelpers/ui/metadata.go new file mode 100644 index 000000000..dec1da15d --- /dev/null +++ b/libs/standalone/protohelpers/ui/metadata.go @@ -0,0 +1,445 @@ +package ui + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "net/http" + "reflect" + "strconv" + "strings" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" +) + +// HeaderPrefix is what a RequestMetadata field is carried under. One header per +// field, named after the field, so nothing here has to be kept in step with the +// struct by hand: the list below is read off the type. +const HeaderPrefix = "X-CRE-REQUEST-METADATA-" + +// Field is one RequestMetadata field, as the UI needs to render it and as the +// wire carries it. +type Field struct { + // Name is the Go field name, which is what MetadataFromHeaders assigns to. + Name string `json:"name"` + // Header is the HTTP header (and gRPC metadata key) this field travels in. + Header string `json:"header"` + // Kind says which input to draw. It is derived from the Go type, so a field + // gets the same box the proto would give it: text for a string, a number for + // a uint32, a datetime for a timestamp. + Kind string `json:"kind"` + // Repeated fields take one header per entry rather than one header holding + // them all, since http.Header and metadata.MD are both map[string][]string. + Repeated bool `json:"repeated"` + // Default is what an unspecified field is filled in with, shown so the UI can + // pre-populate the box with the value that would be sent anyway. + Default string `json:"default"` +} + +// Kinds a Field can have. Anything unrecognised is text: a box the user can type +// into is a worse fit than a number box, but it is never wrong. +const ( + KindText = "text" + KindNumber = "number" + KindTimestamp = "timestamp" + KindPair = "pair" +) + +// Fields is every RequestMetadata field, in declaration order. +// +// Built by reflection rather than listed, so a field added to RequestMetadata +// upstream shows up here - and in the UI, and on the wire - without this package +// being touched. +func Fields() []Field { + t := reflect.TypeFor[capabilities.RequestMetadata]() + defaults := defaultMetadata() + value := reflect.ValueOf(defaults) + + fields := make([]Field, 0, t.NumField()) + for i := range t.NumField() { + f := t.Field(i) + if !f.IsExported() { + continue + } + kind, repeated := kindOf(f.Type) + fields = append(fields, Field{ + Name: f.Name, + Header: HeaderPrefix + headerSuffix(f.Name), + Kind: kind, + Repeated: repeated, + Default: formatDefault(value.Field(i)), + }) + } + return fields +} + +// kindOf maps a Go type to the input the UI draws for it. +func kindOf(t reflect.Type) (kind string, repeated bool) { + if t == reflect.TypeFor[time.Time]() { + return KindTimestamp, false + } + switch t.Kind() { + case reflect.Slice: + // A slice of two-string tuples (SpendLimit) is a pair per entry. + inner, _ := kindOf(t.Elem()) + if inner == KindText && t.Elem().Kind() == reflect.Struct { + return KindPair, true + } + return inner, true + case reflect.Struct: + // SpendLimit is spend type plus limit, both strings, so one box each. + return KindText, false + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return KindNumber, false + default: + return KindText, false + } +} + +// headerSuffix turns a Go field name into a header suffix: WorkflowID becomes +// WORKFLOW-ID, and WorkflowDonConfigVersion becomes WORKFLOW-DON-CONFIG-VERSION. +// +// Word boundaries are kept because HTTP header names are case-insensitive, so +// camel case alone would not survive canonicalisation - Workflowid and +// WorkflowID are the same header, and neither reads as the field it came from. +func headerSuffix(field string) string { + var words []string + runes := []rune(field) + start := 0 + for i := 1; i <= len(runes); i++ { + if i == len(runes) { + words = append(words, string(runes[start:i])) + break + } + prev, cur := runes[i-1], runes[i] + upper := func(r rune) bool { return r >= 'A' && r <= 'Z' } + switch { + case !upper(prev) && upper(cur): + // camelC -> new word + words = append(words, string(runes[start:i])) + start = i + case upper(prev) && upper(cur) && i+1 < len(runes) && !upper(runes[i+1]): + // end of an acronym run: IDValue -> ID, Value + words = append(words, string(runes[start:i])) + start = i + } + } + for i, w := range words { + words[i] = strings.ToUpper(w) + } + return strings.Join(words, "-") +} + +// Fields the report encoder requires as fixed-length hex. A consensus report +// carries the request metadata on-chain, so these are not free-form strings: the +// encoder decodes them and checks the byte count (see chainlink-common's +// consensus/ocr3/types.Metadata.Encode). +// +// Getting this wrong does not fail the request - it fails the round. The plugin +// cannot encode the metadata, so no report is produced, nothing is transmitted +// back, and the request the user made sits until it expires. What they see is a +// timeout, which says nothing about the value that caused it. +const ( + executionIDBytes = 32 + workflowIDBytes = 32 + workflowNameBytes = 10 + workflowOwnerBytes = 20 +) + +// uiMarker is "ui" in ASCII. A value that has to be hex can still say where it +// came from, so a capability's logs name this page rather than showing an +// anonymous run of zeros. +const uiMarker = "7569" + +// defaultMetadata is what a request gets for anything the caller left out. +// +// Every hex field is a valid one of the right length, because "valid" here means +// what the report encoder accepts rather than what looks reasonable. +// +// The execution identifier is not a constant. A capability may well key work, +// dedupe or cache on the execution it was asked under, so two requests sharing one +// would be two runs of the same execution rather than two executions. Every call +// therefore gets its own, and a fan-out settles on one before sending so its +// instances still agree (see HeadersFromMetadata). +func defaultMetadata() capabilities.RequestMetadata { + return capabilities.RequestMetadata{ + WorkflowID: markedHex(workflowIDBytes), + WorkflowOwner: markedHex(workflowOwnerBytes), + OrgID: "ui-org-id", + WorkflowExecutionID: uniqueHex(executionIDBytes), + WorkflowName: markedHex(workflowNameBytes), + WorkflowDonID: 1, + WorkflowDonConfigVersion: 1, + ReferenceID: "ui-reference-id", + DecodedWorkflowName: "ui-workflow-name", + WorkflowTag: "ui-workflow-tag", + WorkflowRegistryChainSelector: "ui-chain-selector", + WorkflowRegistryAddress: markedHex(workflowOwnerBytes), + EngineVersion: "v1", + ExecutionTimestamp: time.Now().UTC(), + } +} + +// markedHex is a stable hex value of exactly n bytes, opening with the marker so +// it is recognisable in a log. +func markedHex(n int) string { + return pad(uiMarker, n) +} + +// uniqueHex is a per-request hex value of exactly n bytes: the marker, then +// randomness. Random rather than a counter, so two processes - the instances of an +// embed run, say - cannot mint the same one. +func uniqueHex(n int) string { + buf := make([]byte, n) + if _, err := rand.Read(buf); err != nil { + // Randomness is unavailable, which is not worth failing a debug request + // over. A timestamp still separates this from the request before it. + return pad(uiMarker+strconv.FormatInt(time.Now().UnixNano(), 16), n) + } + return pad(uiMarker+hex.EncodeToString(buf), n) +} + +// pad trims or zero-fills a hex string to exactly n bytes, which is the length the +// encoder checks. +func pad(value string, n int) string { + width := n * 2 + if len(value) >= width { + return value[:width] + } + return value + strings.Repeat("0", width-len(value)) +} + +// HeadersFromMetadata renders metadata back into the headers it travels in. +// +// This is what lets a fan-out send one metadata to every instance: it resolves the +// defaults once, then sends the result explicitly rather than letting each +// instance fill in its own - which would give each of them a different execution +// ID for what the user asked for as a single request. +func HeadersFromMetadata(md capabilities.RequestMetadata) http.Header { + header := http.Header{} + value := reflect.ValueOf(md) + t := value.Type() + + for i := range t.NumField() { + f := t.Field(i) + if !f.IsExported() { + continue + } + name := HeaderPrefix + headerSuffix(f.Name) + field := value.Field(i) + + if field.Kind() == reflect.Slice && field.Type() != reflect.TypeFor[time.Time]() { + // One header per entry, which is how they are read back. + for j := range field.Len() { + if entry := formatEntry(field.Index(j)); entry != "" { + header.Add(name, entry) + } + } + continue + } + if rendered := formatDefault(field); rendered != "" { + header.Set(name, rendered) + } + } + return header +} + +// formatEntry renders one element of a repeated field, in the key=value form +// assignEntry reads. +func formatEntry(entry reflect.Value) string { + if entry.Kind() == reflect.String { + return entry.String() + } + if entry.Kind() != reflect.Struct || entry.NumField() != 2 { + return "" + } + first, second := entry.Field(0), entry.Field(1) + if first.Kind() != reflect.String || second.Kind() != reflect.String { + return "" + } + return first.String() + "=" + second.String() +} + +// formatDefault renders a default the way the UI would show it, and the way +// MetadataFromHeaders parses it back. +func formatDefault(v reflect.Value) string { + switch { + case v.Type() == reflect.TypeFor[time.Time](): + t := v.Interface().(time.Time) + if t.IsZero() { + return "" + } + return t.UTC().Format(time.RFC3339) + case v.Kind() == reflect.Slice: + return "" + case v.CanUint(): + return strconv.FormatUint(v.Uint(), 10) + case v.CanInt(): + return strconv.FormatInt(v.Int(), 10) + default: + return fmt.Sprint(v.Interface()) + } +} + +// MetadataFromHeaders builds the metadata a capability is called with from the +// headers a request carried. +// +// get is passed the header name and returns every value under it, so this works +// against an http.Header and against gRPC metadata.MD alike - both are +// map[string][]string, and a repeated field is repeated headers rather than one +// header holding a list. +// +// Anything absent or blank is filled in from defaultMetadata, so a caller that +// specifies nothing still sends a usable request. An unparseable number or +// timestamp is an error rather than a silent default: the caller asked for +// something specific and got neither it nor a warning otherwise. +func MetadataFromHeaders(get func(name string) []string) (capabilities.RequestMetadata, error) { + metadata := defaultMetadata() + out := reflect.ValueOf(&metadata).Elem() + t := out.Type() + + for i := range t.NumField() { + f := t.Field(i) + if !f.IsExported() { + continue + } + values := nonBlank(get(HeaderPrefix + headerSuffix(f.Name))) + if len(values) == 0 { + continue + } + if err := assign(out.Field(i), values); err != nil { + return capabilities.RequestMetadata{}, userErrorf("%s: %s", HeaderPrefix+headerSuffix(f.Name), err) + } + } + + // The execution timestamp is the one default that cannot be a constant: a + // fixed one would put every request at the same instant. + if metadata.ExecutionTimestamp.IsZero() { + metadata.ExecutionTimestamp = time.Now().UTC() + } + + if err := validateHexFields(metadata); err != nil { + return capabilities.RequestMetadata{}, err + } + return metadata, nil +} + +// validateHexFields rejects a value the report encoder would refuse. +// +// Checked here so it is reported as the field it is. Left to the capability, the +// same mistake is not an error at all: the consensus plugin cannot encode the +// metadata, so it produces no report, nothing is transmitted back, and the +// request the user made sits until it expires. What they are told is "timeout +// exceeded", which names neither the field nor the length - and takes twenty +// seconds to say it. +func validateHexFields(md capabilities.RequestMetadata) error { + for _, f := range []struct { + name string + value string + bytes int + }{ + {"WorkflowExecutionID", md.WorkflowExecutionID, executionIDBytes}, + {"WorkflowID", md.WorkflowID, workflowIDBytes}, + {"WorkflowName", md.WorkflowName, workflowNameBytes}, + {"WorkflowOwner", md.WorkflowOwner, workflowOwnerBytes}, + } { + decoded, err := hex.DecodeString(strings.TrimPrefix(f.value, "0x")) + if err != nil { + return userErrorf("%s%s: %s must be hex, because a consensus report carries it on-chain: %s", + HeaderPrefix, headerSuffix(f.name), f.name, err) + } + // WorkflowName is padded to length by the encoder, so a short one is fine. + if len(decoded) > f.bytes || (len(decoded) < f.bytes && f.name != "WorkflowName") { + return userErrorf("%s%s: %s must be %d hex bytes (%d characters), got %d", + HeaderPrefix, headerSuffix(f.name), f.name, f.bytes, f.bytes*2, len(decoded)) + } + } + return nil +} + +func nonBlank(values []string) []string { + out := make([]string, 0, len(values)) + for _, v := range values { + if strings.TrimSpace(v) != "" { + out = append(out, v) + } + } + return out +} + +// assign writes header values into one field, by its Go type. +func assign(field reflect.Value, values []string) error { + switch { + case field.Type() == reflect.TypeFor[time.Time](): + parsed, err := time.Parse(time.RFC3339, values[0]) + if err != nil { + return fmt.Errorf("expected an RFC3339 timestamp: %w", err) + } + field.Set(reflect.ValueOf(parsed)) + return nil + + case field.Kind() == reflect.Slice: + slice := reflect.MakeSlice(field.Type(), 0, len(values)) + for _, v := range values { + entry := reflect.New(field.Type().Elem()).Elem() + if err := assignEntry(entry, v); err != nil { + return err + } + slice = reflect.Append(slice, entry) + } + field.Set(slice) + return nil + + case field.CanUint(): + n, err := strconv.ParseUint(values[0], 10, 64) + if err != nil { + return fmt.Errorf("expected a number: %w", err) + } + field.SetUint(n) + return nil + + case field.CanInt(): + n, err := strconv.ParseInt(values[0], 10, 64) + if err != nil { + return fmt.Errorf("expected a number: %w", err) + } + field.SetInt(n) + return nil + + case field.Kind() == reflect.String: + field.SetString(values[0]) + return nil + + default: + return fmt.Errorf("unsupported field type %s", field.Type()) + } +} + +// assignEntry fills one element of a repeated field from a single header value. +// +// A two-string struct (SpendLimit) is "type=limit", the same key=value form the +// standalone config uses for its own pair-valued settings. +func assignEntry(entry reflect.Value, value string) error { + if entry.Kind() == reflect.String { + entry.SetString(value) + return nil + } + if entry.Kind() != reflect.Struct || entry.NumField() != 2 { + return fmt.Errorf("unsupported repeated element type %s", entry.Type()) + } + + key, limit, found := strings.Cut(value, "=") + if !found || key == "" { + return fmt.Errorf("invalid entry %q: expected key=value", value) + } + for i, v := range []string{key, limit} { + f := entry.Field(i) + if f.Kind() != reflect.String { + return fmt.Errorf("unsupported repeated element type %s", entry.Type()) + } + f.SetString(v) + } + return nil +} diff --git a/libs/standalone/protohelpers/ui/metadata_test.go b/libs/standalone/protohelpers/ui/metadata_test.go new file mode 100644 index 000000000..84453664a --- /dev/null +++ b/libs/standalone/protohelpers/ui/metadata_test.go @@ -0,0 +1,274 @@ +package ui + +import ( + "encoding/hex" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + ocr3types "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" +) + +func TestHeaderSuffix(t *testing.T) { + for field, want := range map[string]string{ + "WorkflowID": "WORKFLOW-ID", + "OrgID": "ORG-ID", + "WorkflowDonConfigVersion": "WORKFLOW-DON-CONFIG-VERSION", + "SpendLimits": "SPEND-LIMITS", + "ExecutionTimestamp": "EXECUTION-TIMESTAMP", + "WorkflowRegistryChainSelector": "WORKFLOW-REGISTRY-CHAIN-SELECTOR", + } { + assert.Equal(t, want, headerSuffix(field), field) + } +} + +// Every field of RequestMetadata gets a header, so a field added upstream is +// carried without this package being touched. +func TestFieldsCoverEveryMetadataField(t *testing.T) { + fields := Fields() + require.NotEmpty(t, fields) + + byName := map[string]Field{} + for _, f := range fields { + byName[f.Name] = f + } + + for _, name := range []string{ + "WorkflowID", "WorkflowOwner", "OrgID", "WorkflowExecutionID", "WorkflowName", + "WorkflowDonID", "WorkflowDonConfigVersion", "ReferenceID", "DecodedWorkflowName", + "SpendLimits", "WorkflowTag", "WorkflowRegistryChainSelector", + "WorkflowRegistryAddress", "EngineVersion", "ExecutionTimestamp", + } { + f, ok := byName[name] + require.True(t, ok, "%s has no header", name) + assert.True(t, len(f.Header) > len(HeaderPrefix), "%s: %q", name, f.Header) + } + + assert.Equal(t, KindNumber, byName["WorkflowDonID"].Kind) + assert.Equal(t, KindText, byName["WorkflowID"].Kind) + assert.Equal(t, KindTimestamp, byName["ExecutionTimestamp"].Kind) + assert.True(t, byName["SpendLimits"].Repeated, "spend limits take one header per entry") +} + +// Nothing specified means a request a capability will accept rather than one it +// rejects for a missing field. +func TestMetadataDefaultsWhenNothingIsSpecified(t *testing.T) { + before := time.Now().UTC() + md, err := MetadataFromHeaders(func(string) []string { return nil }) + require.NoError(t, err) + + assert.Equal(t, markedHex(workflowIDBytes), md.WorkflowID) + assert.Equal(t, markedHex(workflowOwnerBytes), md.WorkflowOwner) + assert.NotContains(t, md.WorkflowID, "test", "the defaults are the UI's, not a test's") + assert.EqualValues(t, 1, md.WorkflowDonID) + assert.EqualValues(t, 1, md.WorkflowDonConfigVersion) + assert.NotEmpty(t, md.WorkflowExecutionID) + + // The timestamp is the one default that cannot be a constant. + assert.False(t, md.ExecutionTimestamp.Before(before), "%s", md.ExecutionTimestamp) +} + +func TestMetadataFromHeaders(t *testing.T) { + header := http.Header{} + header.Set(HeaderPrefix+"WORKFLOW-ID", strings.Repeat("cd", workflowIDBytes)) + header.Set(HeaderPrefix+"WORKFLOW-DON-ID", "7") + header.Set(HeaderPrefix+"EXECUTION-TIMESTAMP", "2026-08-19T10:11:12Z") + // Repeated is repeated headers, since a header can hold a list. + header.Add(HeaderPrefix+"SPEND-LIMITS", "CONSENSUS=100000") + header.Add(HeaderPrefix+"SPEND-LIMITS", "COMPUTE=5") + + md, err := MetadataFromHeaders(header.Values) + require.NoError(t, err) + + assert.Equal(t, strings.Repeat("cd", workflowIDBytes), md.WorkflowID) + assert.EqualValues(t, 7, md.WorkflowDonID) + assert.Equal(t, "2026-08-19T10:11:12Z", md.ExecutionTimestamp.Format(time.RFC3339)) + assert.Equal(t, []capabilities.SpendLimit{ + {SpendType: "CONSENSUS", Limit: "100000"}, + {SpendType: "COMPUTE", Limit: "5"}, + }, md.SpendLimits) + + // Anything not sent still falls back. + assert.Equal(t, markedHex(workflowOwnerBytes), md.WorkflowOwner) + assert.EqualValues(t, 1, md.WorkflowDonConfigVersion) +} + +// A blank header is the same as an absent one: the UI leaves empty boxes off the +// wire, and an empty value would otherwise override the default with nothing. +func TestBlankHeadersFallBackToDefaults(t *testing.T) { + header := http.Header{} + header.Set(HeaderPrefix+"WORKFLOW-ID", " ") + + md, err := MetadataFromHeaders(header.Values) + require.NoError(t, err) + assert.Equal(t, markedHex(workflowIDBytes), md.WorkflowID) +} + +// A value the caller asked for that cannot be parsed is an error, not a silent +// default: they would otherwise get neither their value nor a warning. +func TestUnparseableValuesAreErrors(t *testing.T) { + for name, value := range map[string]string{ + "WORKFLOW-DON-ID": "not-a-number", + "EXECUTION-TIMESTAMP": "yesterday", + "SPEND-LIMITS": "no-equals-sign", + } { + header := http.Header{} + header.Set(HeaderPrefix+name, value) + + _, err := MetadataFromHeaders(header.Values) + require.Error(t, err, name) + assert.Contains(t, err.Error(), name) + } +} + +func TestHeaderNamesArePrefixed(t *testing.T) { + names := HeaderNames() + require.Len(t, names, len(Fields())) + for _, n := range names { + assert.Contains(t, n, HeaderPrefix) + } +} + +// Two requests are two executions, so the identifier a capability might key work +// on cannot be shared between them. +func TestExecutionIDIsUniquePerRequest(t *testing.T) { + first, err := MetadataFromHeaders(func(string) []string { return nil }) + require.NoError(t, err) + second, err := MetadataFromHeaders(func(string) []string { return nil }) + require.NoError(t, err) + + assert.NotEqual(t, first.WorkflowExecutionID, second.WorkflowExecutionID) + assert.True(t, strings.HasPrefix(first.WorkflowExecutionID, uiMarker)) + + // The workflow itself is the same workflow, so that one is stable. + assert.Equal(t, first.WorkflowID, second.WorkflowID) +} + +// A caller who names an execution gets the one they named. +func TestExecutionIDCanBeSpecified(t *testing.T) { + header := http.Header{} + mine := strings.Repeat("ef", executionIDBytes) + header.Set(HeaderPrefix+"WORKFLOW-EXECUTION-ID", mine) + + md, err := MetadataFromHeaders(header.Values) + require.NoError(t, err) + assert.Equal(t, mine, md.WorkflowExecutionID) +} + +// Rendering metadata back to headers and reading it again returns what went in. +// This is what a fan-out relies on to send one metadata to several instances. +func TestHeadersRoundTrip(t *testing.T) { + original, err := MetadataFromHeaders(func(string) []string { return nil }) + require.NoError(t, err) + original.SpendLimits = []capabilities.SpendLimit{ + {SpendType: "CONSENSUS", Limit: "100000"}, + {SpendType: "COMPUTE", Limit: "5"}, + } + + header := HeadersFromMetadata(original) + roundTripped, err := MetadataFromHeaders(header.Values) + require.NoError(t, err) + + assert.Equal(t, original.WorkflowExecutionID, roundTripped.WorkflowExecutionID) + assert.Equal(t, original.WorkflowID, roundTripped.WorkflowID) + assert.Equal(t, original.WorkflowDonID, roundTripped.WorkflowDonID) + assert.Equal(t, original.SpendLimits, roundTripped.SpendLimits) + assert.Equal(t, + original.ExecutionTimestamp.Format(time.RFC3339), + roundTripped.ExecutionTimestamp.Format(time.RFC3339)) +} + +// A value that will not parse is the caller's to fix, so it is a 400 rather than +// the page reporting itself as broken. +func TestUnparseableValuesAreUserErrors(t *testing.T) { + header := http.Header{} + header.Set(HeaderPrefix+"WORKFLOW-DON-ID", "not-a-number") + + _, err := MetadataFromHeaders(header.Values) + require.Error(t, err) + assert.True(t, isUserError(err), "%v", err) + assert.Equal(t, http.StatusBadRequest, httpStatus(err)) +} + +// The defaults have to satisfy the report encoder, not merely look plausible. +// +// This is the check that was missing. Values the encoder rejects do not fail the +// request: they fail the consensus round, so no report is produced, nothing is +// transmitted back, and the request expires. The user sees a timeout that says +// nothing about the field that caused it. +func TestDefaultsSatisfyTheReportEncoder(t *testing.T) { + md, err := MetadataFromHeaders(func(string) []string { return nil }) + require.NoError(t, err) + + encoded, err := ocr3types.Metadata{ + Version: 1, + ExecutionID: md.WorkflowExecutionID, + Timestamp: uint32(md.ExecutionTimestamp.Unix()), + DONID: md.WorkflowDonID, + DONConfigVersion: md.WorkflowDonConfigVersion, + WorkflowID: md.WorkflowID, + WorkflowName: md.WorkflowName, + WorkflowOwner: md.WorkflowOwner, + ReportID: "0000", + }.Encode() + require.NoError(t, err, "the defaults must encode: a value the encoder rejects surfaces as a timeout") + assert.NotEmpty(t, encoded) +} + +// The hex fields are the exact byte lengths the encoder checks. +func TestHexDefaultsHaveTheRequiredLengths(t *testing.T) { + md, err := MetadataFromHeaders(func(string) []string { return nil }) + require.NoError(t, err) + + for name, tc := range map[string]struct { + value string + bytes int + }{ + "WorkflowExecutionID": {md.WorkflowExecutionID, executionIDBytes}, + "WorkflowID": {md.WorkflowID, workflowIDBytes}, + "WorkflowName": {md.WorkflowName, workflowNameBytes}, + "WorkflowOwner": {md.WorkflowOwner, workflowOwnerBytes}, + } { + t.Run(name, func(t *testing.T) { + decoded, err := hex.DecodeString(tc.value) + require.NoError(t, err, "%s must be hex, got %q", name, tc.value) + assert.Len(t, decoded, tc.bytes) + // Still says where it came from, despite having to be hex. + assert.True(t, strings.HasPrefix(tc.value, uiMarker), "%s: %q", name, tc.value) + }) + } +} + +// A value the encoder would refuse is reported as the field it is, immediately, +// rather than becoming a consensus timeout twenty seconds later. +func TestHexFieldsAreValidatedUpFront(t *testing.T) { + for name, value := range map[string]string{ + "WORKFLOW-ID": "not-hex", + "WORKFLOW-EXECUTION-ID": "also-not-hex", + "WORKFLOW-OWNER": "0xzz", + } { + t.Run(name, func(t *testing.T) { + header := http.Header{} + header.Set(HeaderPrefix+name, value) + + _, err := MetadataFromHeaders(header.Values) + require.Error(t, err) + assert.True(t, isUserError(err), "%v", err) + assert.Contains(t, err.Error(), name, "the message should name the field") + }) + } + + t.Run("the right length is required", func(t *testing.T) { + header := http.Header{} + header.Set(HeaderPrefix+"WORKFLOW-ID", "abcd") // hex, but 2 bytes not 32 + + _, err := MetadataFromHeaders(header.Values) + require.Error(t, err) + assert.Contains(t, err.Error(), "32 hex bytes") + }) +} diff --git a/libs/standalone/protohelpers/ui/mount.go b/libs/standalone/protohelpers/ui/mount.go new file mode 100644 index 000000000..fe130c259 --- /dev/null +++ b/libs/standalone/protohelpers/ui/mount.go @@ -0,0 +1,356 @@ +package ui + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "html/template" + "io" + "net/http" + "path" + "strings" + "sync" + + "github.com/fullstorydev/grpcui/standalone" +) + +// grpcui keeps these unexported, so the names are repeated here: the fan-out +// speaks the same CSRF scheme as the page it calls into. +const ( + csrfCookieName = "_grpcui_csrf_token" + csrfHeaderName = "x-grpcui-csrf-token" + + // The fan-out page gets its own names. grpcui scopes its cookie to the page's + // own path, so a same-named cookie higher up would be sent alongside it and + // could shadow the page's token. + fanoutCookieName = "_cre_debug_csrf_token" + fanoutHeaderName = "x-cre-debug-csrf-token" +) + +// DefaultPrefix is where the debug pages are mounted. +const DefaultPrefix = "/debug/capabilities" + +// Mount serves this instance's debug page on mux, and adds it to fleet so the +// fan-out page can reach it. +// +// prefix roots both pages: prefix+"/ui/" is the form for this instance's own +// capabilities, and prefix+"/request" is the fan-out over every instance. Both are +// mounted on every instance, so whichever port a browser lands on can drive the +// whole process. +func Mount(mux *http.ServeMux, prefix string, s *Server, fleet *Fleet, index int, title string) error { + if mux == nil { + return fmt.Errorf("a mux is required") + } + if prefix == "" { + prefix = DefaultPrefix + } + prefix = "/" + strings.Trim(prefix, "/") + + served, err := customJS(s) + if err != nil { + return err + } + + page := standalone.Handler( + s, + title, + s.Methods(), + s.Files(), + standalone.AddCSSFile(cssFileName(), func() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(pageCSS)), nil + }), + standalone.AddJSFile(jsFileName(served), func() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(served)), nil + }), + // The metadata a capability is called with comes from the browser, so + // every header a RequestMetadata field travels in has to reach Invoke. + standalone.PreserveHeaders(HeaderNames()), + ) + + uiPath := prefix + "/ui" + mux.Handle(uiPath+"/", http.StripPrefix(uiPath, page)) + + fleet.Add(&Instance{ + Index: index, + Label: fmt.Sprintf("instance %d", index+1), + handler: page, + }) + + f := &fanout{fleet: fleet, prefix: prefix, uiPath: uiPath, server: s} + mux.HandleFunc(prefix+"/request", f.page) + mux.HandleFunc(prefix+"/request/", f.page) + mux.HandleFunc(prefix+"/request/fanout", f.invoke) + mux.HandleFunc(prefix+"/request/s/", f.asset) + + // A bare prefix is otherwise a 404, which reads as the page not being there. + mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, uiPath+"/", http.StatusFound) + }) + return nil +} + +// fanout serves the page that sends one or more requests across every instance. +type fanout struct { + fleet *Fleet + prefix string + uiPath string + server *Server + + once sync.Once + template *template.Template + tmplErr error +} + +// pageConfig is what request.js needs to build the page. +type pageConfig struct { + Instances []*Instance `json:"instances"` + Services map[string][]string `json:"services"` + UIPath string `json:"uiPath"` + Prefix string `json:"prefix"` + // Metadata is every RequestMetadata field, which is what the page's Advanced + // section is built from rather than a hand-written list of inputs. + Metadata []Field `json:"metadata"` +} + +func (f *fanout) config() pageConfig { + services := map[string][]string{} + for key := range f.server.calls { + service, method, found := strings.Cut(key, "/") + if !found { + continue + } + services[service] = append(services[service], method) + } + return pageConfig{ + Instances: f.fleet.List(), + Services: services, + UIPath: f.uiPath, + Prefix: f.prefix, + Metadata: Fields(), + } +} + +func (f *fanout) page(w http.ResponseWriter, r *http.Request) { + f.once.Do(func() { + f.template, f.tmplErr = template.New("request.html").Parse(requestHTML) + }) + if f.tmplErr != nil { + writeError(w, systemErrorf("the debug page template is invalid: %w", f.tmplErr)) + return + } + + ensureCSRFCookie(w, r) + + encoded, err := json.Marshal(f.config()) + if err != nil { + writeError(w, systemErrorf("failed to encode the debug page config: %w", err)) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // Must revalidate, or a rebuild would not reach an open browser. + w.Header().Set("Cache-Control", "no-cache, must-revalidate") + + data := struct { + CSSFile string + JSFile string + UIPath string + Prefix string + Config template.JS + }{ + CSSFile: cssFileName(), + JSFile: requestJSFileName(), + UIPath: f.uiPath, + Prefix: f.prefix, + Config: template.JS(encoded), + } + if err := f.template.Execute(w, data); err != nil { + return + } +} + +// asset serves the fan-out page's own script, and the shared stylesheet, under +// their content-hashed names. +func (f *fanout) asset(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "private, max-age=3600") + switch path.Base(r.URL.Path) { + case requestJSFileName(): + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") + _, _ = io.WriteString(w, requestJS) + case cssFileName(): + w.Header().Set("Content-Type", "text/css; charset=utf-8") + _, _ = io.WriteString(w, pageCSS) + default: + http.NotFound(w, r) + } +} + +// requestGroup is one request body plus the instances it is addressed to. +type requestGroup struct { + Instances []int `json:"instances"` + Body json.RawMessage `json:"body"` +} + +type fanoutRequest struct { + // Method is dot-separated, the way grpcui's invoke route expects it. + Method string `json:"method"` + Groups []requestGroup `json:"groups"` + // Metadata is the request metadata every group is sent with. The page fills it + // in before sending, so each instance is called with the same metadata and a + // difference between their answers is the capability's rather than the call's. + Metadata map[string][]string `json:"metadata"` +} + +// instanceResult is one row of the fan-out. Status is "ok" when the instance +// answered, "error" when the call failed, and "na" when it was not addressed. +type instanceResult struct { + Instance int `json:"instance"` + Label string `json:"label"` + Status string `json:"status"` + Group int `json:"group"` + Response json.RawMessage `json:"response,omitempty"` + Error string `json:"error,omitempty"` +} + +type fanoutResponse struct { + Method string `json:"method"` + Results []instanceResult `json:"results"` +} + +func (f *fanout) invoke(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "POST only", http.StatusMethodNotAllowed) + return + } + // The same CSRF shape grpcui uses, so this is no easier to drive from a + // hostile page than the pages behind it. + cookie, err := r.Cookie(fanoutCookieName) + if err != nil || cookie.Value == "" || cookie.Value != r.Header.Get(fanoutHeaderName) { + http.Error(w, "incorrect CSRF token", http.StatusUnauthorized) + return + } + + var req fanoutRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, userErrorf("bad request body: %s", err)) + return + } + if req.Method == "" { + writeError(w, userErrorf("method is required")) + return + } + + // Resolved here, once, rather than letting each instance fill in its own + // defaults: the unspecified fields include the execution ID, so instances left + // to their own devices would each invent a different one and what the user + // asked for as a single request would arrive as several. Doing it here also + // means a value that will not parse is one 400 rather than the same complaint + // from every instance. + metadata, err := MetadataFromHeaders(func(name string) []string { return req.Metadata[name] }) + if err != nil { + writeError(w, err) + return + } + header := HeadersFromMetadata(metadata) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(f.run(req, header)); err != nil { + return + } +} + +// run sends each group to the instances it names, and returns one row per +// instance so an unaddressed one still reports N/A. +// +// Every instance is called at once, not one after another. An OCR capability +// cannot answer one instance until enough of the others have joined the same +// round, so asking them in turn would leave the first waiting for a quorum that +// has not been invited yet - the call would time out rather than return. Even for +// a capability that could answer alone, "all of them at once" is what a fan-out +// is for. +func (f *fanout) run(req fanoutRequest, header http.Header) fanoutResponse { + all := f.fleet.List() + byIndex := map[int]*Instance{} + for _, in := range all { + byIndex[in.Index] = in + } + + // The work is collected before any of it runs, so the whole fan-out starts + // together rather than a group at a time. + type job struct { + instance *Instance + group int + body []byte + } + var jobs []job + claimed := map[int]bool{} + for gi, group := range req.Groups { + for _, idx := range group.Instances { + in, ok := byIndex[idx] + if !ok || claimed[idx] { + // Unknown instance, or one an earlier group already claimed: the + // page keeps the groups disjoint, this is the backstop. + continue + } + claimed[idx] = true + jobs = append(jobs, job{instance: in, group: gi + 1, body: group.Body}) + } + } + + rows := make(map[int]instanceResult, len(jobs)) + var mu sync.Mutex + var wg sync.WaitGroup + for _, j := range jobs { + wg.Add(1) + go func(j job) { + defer wg.Done() + + row := instanceResult{ + Instance: j.instance.Index, + Label: j.instance.Label, + Group: j.group, + Status: "ok", + } + response, err := j.instance.invoke(req.Method, j.body, header) + if err != nil { + row.Status = "error" + row.Error = err.Error() + } else { + row.Response = response + } + + mu.Lock() + defer mu.Unlock() + rows[j.instance.Index] = row + }(j) + } + wg.Wait() + + out := fanoutResponse{Method: req.Method, Results: make([]instanceResult, 0, len(all))} + for _, in := range all { + if row, ok := rows[in.Index]; ok { + out.Results = append(out.Results, row) + continue + } + out.Results = append(out.Results, instanceResult{Instance: in.Index, Label: in.Label, Status: "na"}) + } + return out +} + +// ensureCSRFCookie mirrors what grpcui does for its own pages, so the fan-out page +// has a token to send back. +func ensureCSRFCookie(w http.ResponseWriter, r *http.Request) { + if _, err := r.Cookie(fanoutCookieName); err == nil { + return + } + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + return + } + http.SetCookie(w, &http.Cookie{ + Name: fanoutCookieName, + Value: base64.RawURLEncoding.EncodeToString(buf), + Path: "/", + }) +} diff --git a/libs/standalone/protohelpers/ui/resources/form.js b/libs/standalone/protohelpers/ui/resources/form.js new file mode 100644 index 000000000..bc3c711e2 --- /dev/null +++ b/libs/standalone/protohelpers/ui/resources/form.js @@ -0,0 +1,426 @@ +// Per-instance capability form. +// +// grpcui renders the form; this adds what a capability call needs on top of it: +// every optional field marked present, the request metadata a host would have +// carried made editable, and - when embedded in the fan-out page - hooks for the +// parent to read and write a request body. +// +// Everything type-related comes from window.__CRE_DEBUG__, which the server builds +// from the RequestMetadata type and the capability descriptors. Nothing here +// infers a type from the shape of the data. + +document.addEventListener("DOMContentLoaded", function () { + var CONFIG = window.__CRE_DEBUG__ || { metadata: [], headerPrefix: "" }; + + // ---- request metadata ---------------------------------------------------- + // + // The metadata travels as one header per field, which grpcui forwards because + // the server passed those names to PreserveHeaders. Each field is mirrored + // into a row of grpcui's own (hidden) metadata table, so it rides along with + // the request the same way a hand-typed header would. + + function ensureAdvancedSection() { + if ($("#cre-advanced").length) { + return; + } + var $metadata = $("#grpc-request-metadata"); + var $invoke = $("#grpc-request-tab > button.grpc-invoke").first(); + if (!$metadata.length || !$invoke.length) { + return; + } + + // The "Request Metadata" h3 has no id, so tag it for the CSS to hide. + $metadata.prev("h3").addClass("cre-hidden"); + + var $details = $("
", { id: "cre-advanced" }); + $details.append($("", { text: "Advanced" })); + $details.append($("
", { + "class": "cre-metadata-note", + text: "Request metadata. Anything left blank is filled in with a valid value by the server." + })); + + var $grid = $("
", { "class": "cre-metadata" }); + CONFIG.metadata.forEach(function (field) { + var $row = $("", { "class": "cre-metadata-field" }); + $row.append($("
+ + diff --git a/libs/standalone/protohelpers/ui/resources/request.js b/libs/standalone/protohelpers/ui/resources/request.js new file mode 100644 index 000000000..f010f4a07 --- /dev/null +++ b/libs/standalone/protohelpers/ui/resources/request.js @@ -0,0 +1,814 @@ +// Fan-out page: one or more requests, sent across every instance this process runs. +// +// Each request is the real per-instance page, embedded in a same-origin iframe. +// That is deliberate: grpcui builds its form from a closure keyed to a single +// #grpc-request-form, so it cannot be asked to render a second form on one page. +// An iframe gives a genuine duplicate - same widgets, same validation - and because +// it is same-origin the body can be read straight out of it through the hooks +// form.js exports. +// +// The metadata is the page's own, not each request's: it is filled in here, sent +// with every group, and so identical across instances. A difference between their +// answers is then the capability's rather than the call's. + +$(function () { + var CONFIG = window.__CRE_REQUEST__ || { + instances: [], services: {}, uiPath: "", prefix: "", metadata: [] + }; + var COOKIE = "_cre_debug_csrf_token"; + var HEADER = "x-cre-debug-csrf-token"; + + // ---- state --------------------------------------------------------------- + // + // selected : instances the fan-out addresses at all. Unselected ones report + // N/A without being contacted. + // mirror : one body for every selected instance. + // requests : the per-request claim sets, always disjoint, so an instance + // belongs to exactly one request. + // + // A request's uid never changes: it keys the live iframe, so re-rendering never + // reloads a form already filled in. The number shown is the position in the + // list, so numbering is always 1..N with no gaps. + + var state = { + selected: CONFIG.instances.map(function (i) { return i.index; }), + mirror: true, + requests: [{ uid: 1, claims: CONFIG.instances.map(function (i) { return i.index; }) }] + }; + var nextUid = 2; + var requestEls = {}; + + // ---- history persistence ------------------------------------------------- + // + // The same scheme grpcui uses for its own History tab: localStorage, capped at + // maxHistory entries and maxHistorySize bytes, dropping the oldest until it + // fits, with every access guarded because incognito mode and a full quota both + // throw. + + var HISTORY_KEY = "cre-debug-history-" + window.location.host; + var maxHistory = 100; + var maxHistorySize = 1024 * 1024; + var history = []; + + function getStorageItem(key) { + try { + return localStorage.getItem(key); + } catch (e) { + return null; + } + } + + function setStorageItem(key, value) { + try { + localStorage.setItem(key, value); + } catch (e) { + console.trace(e); + } + } + + function loadHistory() { + var json = getStorageItem(HISTORY_KEY); + if (!json) { + return; + } + try { + var parsed = JSON.parse(json); + if (parsed instanceof Array) { + history = parsed; + } + } catch (e) { + history = []; + } + } + + function onHistoryChange() { + var data; + while (true) { + data = JSON.stringify(history); + if (data.length <= maxHistorySize || history.length === 0) { + break; + } + history = history.slice(0, history.length - 1); + } + setStorageItem(HISTORY_KEY, data); + renderHistory(); + } + + function addHistory(item) { + history = history.slice(0, maxHistory - 1); + history.unshift(item); + onHistoryChange(); + } + + // ---- helpers ------------------------------------------------------------- + + function instanceByIndex(idx) { + return CONFIG.instances.filter(function (i) { return i.index === idx; })[0]; + } + + function isSelected(idx) { + return state.selected.indexOf(idx) !== -1; + } + + function claimsOf(request) { + return request.claims.filter(isSelected); + } + + function unclaimed() { + var taken = {}; + state.requests.forEach(function (r) { + claimsOf(r).forEach(function (i) { taken[i] = true; }); + }); + return state.selected.filter(function (i) { return !taken[i]; }); + } + + function currentMethodPath() { + var service = $("#grpc-service").val(); + var method = $("#grpc-method").val(); + if (!service || !method) { + return null; + } + return service + "." + method; + } + + function iframeSrc() { + return CONFIG.uiPath + "/?embed=1&serviceName=" + encodeURIComponent($("#grpc-service").val()) + + "&methodName=" + encodeURIComponent($("#grpc-method").val()); + } + + // ---- request cards ------------------------------------------------------- + + function buildRequestCard(request) { + var $card = $("
", { "class": "cre-request", "data-request-uid": request.uid }); + + var $head = $("
", { "class": "cre-request-head" }); + var $title = $("", { "class": "cre-request-title" }); + var $copy = $("", { "class": "cre-request-copy" }); + var $targets = $("", { "class": "cre-request-targets" }); + $head.append($title).append($copy).append($targets); + $card.append($head); + + var $frame = $("