diff --git a/.gitignore b/.gitignore index 9536aafa4..fc33313ab 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ **/.tools *.wasm +# Locally built binaries (go build leaves them next to their main package) +/crecore/crecore +/consensus/consensus +/cron/cron + # IntelliJ IDE .idea .vscode/ @@ -30,6 +35,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/.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/chain_capabilities/evm/actions/actions.go b/chain_capabilities/evm/actions/actions.go index 2678c2b22..7f34b63de 100644 --- a/chain_capabilities/evm/actions/actions.go +++ b/chain_capabilities/evm/actions/actions.go @@ -17,7 +17,6 @@ import ( "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/v2/chain-capabilities/evm" evmservice "github.com/smartcontractkit/chainlink-common/pkg/chains/evm" commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -30,6 +29,8 @@ import ( "github.com/smartcontractkit/chainlink-framework/multinode" valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + evm "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" "github.com/smartcontractkit/capabilities/chain_capabilities/evm/config" diff --git a/chain_capabilities/evm/actions/actions_internal_test.go b/chain_capabilities/evm/actions/actions_internal_test.go index b5af46c94..d6cbec682 100644 --- a/chain_capabilities/evm/actions/actions_internal_test.go +++ b/chain_capabilities/evm/actions/actions_internal_test.go @@ -9,19 +9,21 @@ import ( "testing" "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - evmprotos "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" "github.com/smartcontractkit/chainlink-framework/multinode" valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + evmprotos "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/capabilities/libs/chainconsensus/types" ) diff --git a/chain_capabilities/evm/actions/actions_test.go b/chain_capabilities/evm/actions/actions_test.go index a3e516386..edb248a85 100644 --- a/chain_capabilities/evm/actions/actions_test.go +++ b/chain_capabilities/evm/actions/actions_test.go @@ -33,9 +33,10 @@ import ( "google.golang.org/protobuf/testing/protocmp" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + + evmcappb "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" ) func TestCapability_CallContract(t *testing.T) { diff --git a/chain_capabilities/evm/actions/write_report.go b/chain_capabilities/evm/actions/write_report.go index 320180d14..8a9dacfe2 100644 --- a/chain_capabilities/evm/actions/write_report.go +++ b/chain_capabilities/evm/actions/write_report.go @@ -15,12 +15,13 @@ import ( "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/v2/chain-capabilities/evm" commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/contexts" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" "github.com/smartcontractkit/chainlink-common/pkg/types" + evm "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/chainlink-common/pkg/logger" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" diff --git a/chain_capabilities/evm/actions/write_report_test.go b/chain_capabilities/evm/actions/write_report_test.go index 7c3b59bf4..197456102 100644 --- a/chain_capabilities/evm/actions/write_report_test.go +++ b/chain_capabilities/evm/actions/write_report_test.go @@ -18,7 +18,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" ocrtypes "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/contexts" "github.com/smartcontractkit/chainlink-common/pkg/settings" @@ -26,6 +25,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" workflowpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + evm "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + p2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" diff --git a/chain_capabilities/evm/capability.go b/chain_capabilities/evm/capability.go new file mode 100644 index 000000000..c1da4f941 --- /dev/null +++ b/chain_capabilities/evm/capability.go @@ -0,0 +1,344 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + commontypes2 "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/actions" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/config" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/height" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/monitoring" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/trigger" + "github.com/smartcontractkit/capabilities/libs/chainconsensus" + consMetrics "github.com/smartcontractkit/capabilities/libs/chainconsensus/metrics" + "github.com/smartcontractkit/capabilities/libs/chainconsensus/oracle" + "github.com/smartcontractkit/capabilities/libs/chainconsensus/poller" + libsocr "github.com/smartcontractkit/capabilities/libs/ocr" +) + +// CapabilityName is what this binary calls itself in logs and metrics. The +// capability's registered ID is not this: it carries the chain selector, since a +// workflow asks for a chain rather than for EVM in general. +const CapabilityName = "evm" + +// localOCRConfig is what this capability's oracle runs under. The values are the +// node's own defaults: the rounds here agree a block height and hand back a +// chain read, so nothing about them argues for a different pace than any other +// capability's. +var localOCRConfig = ocrtypes.LocalConfig{ + BlockchainTimeout: 20 * time.Second, + ContractConfigTrackerPollInterval: 10 * time.Second, + ContractConfigConfirmations: 1, + ContractTransmitterTransmitTimeout: 10 * time.Second, + DatabaseTimeout: 10 * time.Second, + ContractConfigLoadTimeout: 10 * time.Second, + DefaultMaxDurationInitialization: 10 * time.Second, +} + +// Dependencies are what an EVM capability needs from wherever it is hosted: the +// chain it reads and writes, the OCR configuration and identity it agrees under, +// and the node-shaped things it is not able to be. +// +// 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 { + // EVMService is the chain: reads, log tracking and transaction submission. + // Built from chainlink-evm's own components rather than reached through a + // relayer, which is why this process needs a database and keys of its own. + EVMService commontypes2.EVMService + + // ChainInfo names that chain, for the telemetry every message carries. + ChainInfo commontypes2.ChainInfo + + // DonID is the capability DON this process was spawned for, which together + // with the capability ID selects this oracle's configuration, and which the + // trigger events are labelled with as the sending DON. + DonID uint32 + + // Registry supplies that configuration, digest included, and is also where the + // transmission scheduler reads this DON's membership from. + Registry core.OCRConfigRegistry + + // CapabilityRegistry resolves the DON this capability belongs to, which is + // what staggered transmission needs to know its place in. + CapabilityRegistry core.CapabilitiesRegistry + + // Endpoints, Offchain and Onchain come from whoever holds the node's peer: + // the transport, and the keys this oracle signs with. TransmitAccount is the + // account the configuration lists this node under. + Endpoints ocrtypes.BinaryNetworkEndpointFactory + Offchain ocrtypes.OffchainKeyring + Onchain ocr3types.OnchainKeyring[[]byte] + 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 + + // EventStore holds trigger events that have fired but not been acknowledged, + // so a restart does not drop what was in flight. + EventStore capabilities.EventStore + + 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 + // rest of the capability without a DON to agree with. + 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 +} + +// evmCapability is the EVM chain capability: the actions a workflow calls, the +// log trigger it registers, and the oracle the nodes agree a block height with. +type evmCapability struct { + *actions.EVM + + lggr logger.Logger + id string + chainSelector uint64 + + requestPoller *poller.Poller + consensusHandler chainconsensus.Handler + oracle Oracle + triggerService *trigger.LogTriggerService + heightProvider *height.Provider +} + +var _ protos.ClientCapability = (*evmCapability)(nil) + +// New builds the capability over the chain and identity it is given. +// +// Everything is built here and started by Start: building needs the chain and the +// configuration, and starting means joining a protocol and polling a chain - +// things to do when the process is ready to serve rather than while it is still +// being assembled. +func New(lggr logger.Logger, cfg config.Config, deps Dependencies) (*evmCapability, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + + chainID, err := strconv.ParseUint(deps.ChainInfo.ChainID, 10, 64) + if err != nil { + return nil, fmt.Errorf("chain %q is not an EVM chain ID: %w", deps.ChainInfo.ChainID, err) + } + chainSelector, ok := chainselectors.EvmChainIdToChainSelector()[chainID] + if !ok { + return nil, fmt.Errorf("no chain selector for chain ID %d", chainID) + } + + metrics, err := monitoring.NewMetrics() + if err != nil { + return nil, fmt.Errorf("failed to create metrics: %w", err) + } + processor, err := monitoring.NewProcessor(lggr, metrics) + if err != nil { + return nil, fmt.Errorf("failed to create monitoring proto processor: %w", err) + } + + c := &evmCapability{ + lggr: lggr, + id: CapabilityName + ":ChainSelector:" + strconv.FormatUint(chainSelector, 10) + "@1.0.0", + chainSelector: chainSelector, + } + + capInfo, err := capabilities.NewCapabilityInfo(c.id, capabilities.CapabilityTypeCombined, "Contains EVM chain functionalities") + if err != nil { + return nil, fmt.Errorf("failed to describe capability %s: %w", c.id, err) + } + messageBuilder := monitoring.NewMessageBuilder(deps.ChainInfo, capInfo, cfg.NodeAddress) + + consensusMetrics, err := consMetrics.NewConsensusMetrics(deps.ChainInfo) + if err != nil { + return nil, fmt.Errorf("failed to create evm consensus metrics: %w", err) + } + c.requestPoller = poller.NewPoller(lggr, consensusMetrics, cfg.ObservationPollerWorkersCount, cfg.ObservationPollPeriod) + c.consensusHandler = chainconsensus.NewHandler(lggr, c.requestPoller, consensusMetrics, cfg.UnknownRequestsTTL) + + scheduler, err := c.transmissionScheduler(cfg, deps) + if err != nil { + return nil, err + } + + c.EVM, err = actions.NewEVM(cfg, deps.EVMService, lggr, processor, messageBuilder, c.consensusHandler, + chainSelector, deps.LimitsFactory, scheduler) + if err != nil { + return nil, fmt.Errorf("failed to create EVM actions for chain %d: %w", chainID, err) + } + + // TODO: add org resolver + c.triggerService, err = trigger.NewLogTriggerService(deps.EVMService, trigger.NewLogTriggerStore(), lggr, + fmt.Sprintf("%s (%d)", c.id, chainID), deps.DonID, processor, messageBuilder, + cfg.LogTriggerPollInterval, cfg.LogTriggerSendChannelBufferSize, cfg.LogTriggerLimitQueryLogSize, + deps.LimitsFactory, nil, deps.EventStore) + if err != nil { + return nil, fmt.Errorf("failed to create the log trigger: %w", err) + } + + c.heightProvider = height.NewProvider(lggr, cfg.ChainHeightPollPeriod, deps.EVMService) + + newOracle := deps.NewOracle + if newOracle == nil { + newOracle = func(args libsocr.OracleArgs) (Oracle, error) { return libsocr.NewOracle(args) } + } + + c.oracle, err = newOracle(libsocr.OracleArgs{ + CapabilityID: c.id, + DonID: deps.DonID, + Registry: deps.Registry, + Endpoints: deps.Endpoints, + Offchain: deps.Offchain, + Onchain: deps.Onchain, + TransmitAccount: deps.TransmitAccount, + Bootstrappers: deps.Bootstrappers, + Plugin: oracle.NewReportingPluginFactory(logger.Sugared(lggr), c.consensusHandler, c.heightProvider, consensusMetrics), + Transmitter: oracle.NewContractTransmitter(lggr, c.consensusHandler), + LocalConfig: localOCRConfig, + Logger: lggr, + Metrics: deps.Metrics, + }) + if err != nil { + return nil, fmt.Errorf("failed to create the oracle: %w", err) + } + + return c, nil +} + +// transmissionScheduler staggers what this node writes to the chain, so a DON +// answering one request does not send the same transaction from every member at +// once. +// +// It is optional: without a stage delay there is nothing to stagger, and the +// scheduler is left nil rather than built as a no-op. +func (c *evmCapability) transmissionScheduler(cfg config.Config, deps Dependencies) (ts.TransmissionScheduler, error) { + if cfg.DeltaStage <= 0 { + c.lggr.Infow("DeltaStage not configured, transmission scheduling disabled") + return ts.TransmissionScheduler{}, nil + } + + // Staggering means knowing this DON's membership and quorum, and which member + // this node is. The authoritative DON ID is passed so that a node belonging to + // several DONs running this capability picks the right one. + ctx := context.Background() + myDON, err := ts.InitMyDON(ctx, deps.CapabilityRegistry, c.id, deps.DonID, c.lggr, cfg.IsLocal) + if err != nil { + return ts.TransmissionScheduler{}, fmt.Errorf("failed to init DON: %w", err) + } + c.lggr.Debugw("Initialised DON", "donID", myDON.ID, "donName", myDON.Name, "members", len(myDON.Members), "F", myDON.F) + + scheduler, err := ts.InitialiseTransmissionScheduler(ctx, deps.CapabilityRegistry, cfg.DeltaStage, c.lggr, &myDON, cfg.IsLocal) + if err != nil { + return ts.TransmissionScheduler{}, fmt.Errorf("failed to initialize transmission scheduler: %w", err) + } + return scheduler, nil +} + +// Start runs everything the capability answers with: the consensus round that +// agrees a block height, the poller feeding it, the oracle running it, and the +// log trigger reading the chain. +// +// The chain itself is not started here. It is a service of the process rather +// than of this capability - the same database and the same log poller would back +// a second capability in this binary - so whoever built it starts it. +func (c *evmCapability) Start(ctx context.Context) error { + started := []interface{ Close() error }{} + for _, service := range []interface { + Start(context.Context) error + Close() error + }{c.consensusHandler, c.requestPoller, c.oracleService(), c.heightProvider, c.triggerService} { + if err := service.Start(ctx); err != nil { + // Whatever did start is stopped again: a capability that failed to start is + // not running, and leaving half of it polling a chain would make it look like + // it was. + for i := len(started) - 1; i >= 0; i-- { + if cerr := started[i].Close(); cerr != nil { + c.lggr.Errorw("Failed to stop a service after a failed start", "err", cerr) + } + } + return err + } + started = append(started, service) + } + + c.lggr.Infof("Started %s", CapabilityName) + return nil +} + +func (c *evmCapability) Close() error { + return errors.Join( + c.EVM.Close(), + c.requestPoller.Close(), + c.consensusHandler.Close(), + c.oracle.Close(), + c.triggerService.Close(), + c.heightProvider.Close(), + ) +} + +// oracleService adapts the oracle to the Start/Close pair everything else here +// has: libocr's takes no context to start and none to stop. +func (c *evmCapability) oracleService() interface { + Start(context.Context) error + Close() error +} { + return oracleService{c.oracle} +} + +type oracleService struct{ oracle Oracle } + +func (o oracleService) Start(context.Context) error { return o.oracle.Start() } + +func (o oracleService) Close() error { return o.oracle.Close() } + +func (c *evmCapability) HealthReport() map[string]error { + return map[string]error{c.Name(): nil} +} + +func (c *evmCapability) Name() string { return c.lggr.Name() } + +func (c *evmCapability) Ready() error { return nil } + +// ChainSelector is what makes this capability's ID say which chain it is. The +// generated server reads it, so a workflow asking for a chain reaches the binary +// running that chain. +func (c *evmCapability) ChainSelector() uint64 { return c.chainSelector } + +func (c *evmCapability) Description() string { return "Contains EVM chain functionalities" } + +func (c *evmCapability) RegisterLogTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *protos.FilterLogTriggerRequest) (<-chan capabilities.TriggerAndId[*protos.Log], caperrors.Error) { + return c.triggerService.RegisterLogTrigger(ctx, triggerID, metadata, input) +} + +func (c *evmCapability) UnregisterLogTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *protos.FilterLogTriggerRequest) caperrors.Error { + return c.triggerService.UnregisterLogTrigger(ctx, triggerID, metadata, input) +} + +func (c *evmCapability) AckEvent(ctx context.Context, triggerID string, eventID string, _ string) caperrors.Error { + return c.triggerService.AckEvent(ctx, triggerID, eventID) +} diff --git a/chain_capabilities/evm/chain/account.go b/chain_capabilities/evm/chain/account.go new file mode 100644 index 000000000..6fc6c65af --- /dev/null +++ b/chain_capabilities/evm/chain/account.go @@ -0,0 +1,82 @@ +package chain + +import ( + "context" + "fmt" + "strings" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// narrowed is the keystore this chain signs with: the node's, holding only the +// account this chain sends from. +// +// A node holds a key per chain it runs, and the keystore this process borrows is +// one flat store of all of them - the chain each belongs to lived in the node's +// own evm.key_states table, which is where a relayer's keystore was narrowed +// (see core's relayer_factory, which builds one EthSigner per chain). Reaching +// the chain directly means that narrowing has to happen here instead. +// +// It is not tidiness. chainlink-evm sends from the enabled address with the +// highest balance, so a chain left holding another chain's key can decide to send +// as an account that is not this node's transmitter here. +// +// An empty account leaves the keystore alone: an embedded instance derives one +// key and has nothing to narrow. +func narrowed(ctx context.Context, keystore core.Keystore, account *string) (core.Keystore, error) { + if account == nil || *account == "" { + return keystore, nil + } + + accounts, err := keystore.Accounts(ctx) + if err != nil { + return nil, fmt.Errorf("failed to read the accounts this node holds: %w", err) + } + // Checked here rather than left to the first transaction: an account this node + // does not hold is a configuration this chain cannot write with, and saying so at + // startup is the difference between a chain that does not start and one that reads + // happily until the first report has to land. + if !holds(accounts, *account) { + return nil, fmt.Errorf("this node holds no key for account %s, which is the account this chain sends from", *account) + } + + return &oneAccount{keystore: keystore, account: *account}, nil +} + +func holds(accounts []string, account string) bool { + for _, held := range accounts { + if strings.EqualFold(held, account) { + return true + } + } + return false +} + +// oneAccount is the node's keystore, seen through the one account this chain uses. +type oneAccount struct { + keystore core.Keystore + account string +} + +var _ core.Keystore = (*oneAccount)(nil) + +func (k *oneAccount) Accounts(context.Context) ([]string, error) { + return []string{k.account}, nil +} + +// Sign refuses the node's other accounts rather than passing them through: they +// belong to this node's other chains, and a transaction on this one signed by one +// of them is a nonce this chain does not track. +func (k *oneAccount) Sign(ctx context.Context, account string, data []byte) ([]byte, error) { + if !strings.EqualFold(account, k.account) { + return nil, fmt.Errorf("this chain sends from %s, not %s", k.account, account) + } + return k.keystore.Sign(ctx, k.account, data) +} + +func (k *oneAccount) Decrypt(ctx context.Context, account string, data []byte) ([]byte, error) { + if !strings.EqualFold(account, k.account) { + return nil, fmt.Errorf("this chain uses %s, not %s", k.account, account) + } + return k.keystore.Decrypt(ctx, k.account, data) +} diff --git a/chain_capabilities/evm/chain/account_test.go b/chain_capabilities/evm/chain/account_test.go new file mode 100644 index 000000000..d91993fe2 --- /dev/null +++ b/chain_capabilities/evm/chain/account_test.go @@ -0,0 +1,90 @@ +package chain + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// nodeKeystore is the keystore a node lends this process: every key it has, for +// every chain it runs. +type nodeKeystore struct { + accounts []string + signed string +} + +var _ core.Keystore = (*nodeKeystore)(nil) + +func (k *nodeKeystore) Accounts(context.Context) ([]string, error) { return k.accounts, nil } + +func (k *nodeKeystore) Sign(_ context.Context, account string, _ []byte) ([]byte, error) { + k.signed = account + return []byte("signature"), nil +} + +func (k *nodeKeystore) Decrypt(context.Context, string, []byte) ([]byte, error) { + return nil, errors.New("chain keys sign; they do not decrypt") +} + +const ( + thisChain = "0x1111111111111111111111111111111111111111" + otherChain = "0x2222222222222222222222222222222222222222" +) + +// TestNarrowed covers what a node's key states do for a relayer: the chain sees +// the account enabled for it, and not the ones enabled for its other chains. +// +// chainlink-evm sends from the enabled address with the highest balance, so an +// unfiltered store is not untidy but wrong - this chain would send as an account +// belonging to another. +func TestNarrowed(t *testing.T) { + node := &nodeKeystore{accounts: []string{otherChain, thisChain}} + account := thisChain + + ks, err := narrowed(t.Context(), node, &account) + require.NoError(t, err) + + accounts, err := ks.Accounts(t.Context()) + require.NoError(t, err) + assert.Equal(t, []string{thisChain}, accounts) + + t.Run("signs as its own account", func(t *testing.T) { + _, err := ks.Sign(t.Context(), thisChain, []byte("a transaction")) + require.NoError(t, err) + assert.Equal(t, thisChain, node.signed) + }) + + t.Run("refuses another chain's", func(t *testing.T) { + node.signed = "" + _, err := ks.Sign(t.Context(), otherChain, []byte("a transaction")) + require.ErrorContains(t, err, "this chain sends from "+thisChain) + assert.Empty(t, node.signed, "the node's key must not have been reached") + }) +} + +// TestNarrowedMustBeHeld is the startup check: a chain configured with an account +// this node has no key for cannot write, and finding that out at the first report +// is finding out too late. +func TestNarrowedMustBeHeld(t *testing.T) { + node := &nodeKeystore{accounts: []string{otherChain}} + account := thisChain + + _, err := narrowed(t.Context(), node, &account) + require.ErrorContains(t, err, "this node holds no key for account "+thisChain) +} + +// TestNarrowedUnset is the embedded run: one derived key, nothing to narrow. +func TestNarrowedUnset(t *testing.T) { + node := &nodeKeystore{accounts: []string{thisChain}} + + for _, account := range []*string{nil, new(string)} { + ks, err := narrowed(t.Context(), node, account) + require.NoError(t, err) + assert.Same(t, core.Keystore(node), ks) + } +} diff --git a/chain_capabilities/evm/chain/chain.go b/chain_capabilities/evm/chain/chain.go new file mode 100644 index 000000000..ac6da41cf --- /dev/null +++ b/chain_capabilities/evm/chain/chain.go @@ -0,0 +1,351 @@ +package chain + +import ( + "context" + "database/sql" + "errors" + "fmt" + "math/big" + "time" + + "github.com/jmoiron/sqlx" + "github.com/scylladb/go-reflectx" + + "github.com/smartcontractkit/chainlink-evm/pkg/chains/legacyevm" + evmclient "github.com/smartcontractkit/chainlink-evm/pkg/client" + "github.com/smartcontractkit/chainlink-evm/pkg/config/toml" + "github.com/smartcontractkit/chainlink-evm/pkg/keys" + "github.com/smartcontractkit/chainlink-evm/pkg/relay" + + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" +) + +// Config is what the chain needs beyond the RPC connection and the database: the +// few settings a deployment chooses rather than inherits. +// +// Everything else comes from chainlink-evm's own defaults for the chain in +// question (toml.Defaults), the same set a node starts from. Exposing all of it +// would mean a flag per field of a configuration that exists to be defaulted; a +// deployment that needs one of those is a reason to add it here, one at a time. +type Config struct { + // LogPollInterval is how often the log poller asks for new blocks. It is here + // rather than defaulted because it is the setting a deployment feels: it is the + // floor on how quickly a log trigger can fire. + LogPollInterval commonconfig.Duration `usage:"how often the log poller reads new blocks, which is the floor on log trigger latency"` + + // FinalityDepth and FinalityTagEnabled decide when this capability calls a block + // final, which is what a workflow asking for finalized state is answered from. + FinalityTagEnabled bool `usage:"use the finalized block tag instead of a finality depth"` + FinalityDepth uint32 `usage:"blocks behind the head that count as final, used when --chain.finality-tag-enabled=false"` + + // NoNewHeadsThreshold is how long without a head before the chain is declared + // unreachable. Zero leaves chainlink-evm's own answer for this chain. + NoNewHeadsThreshold commonconfig.Duration `usage:"how long without a new head before the chain is treated as unreachable; 0 keeps the chain's own default"` + + // TransactionsEnabled says whether this process may write to the chain. A + // capability that only reads and watches logs is better off saying so, since a + // transaction manager it never uses is still a transaction manager watching + // heads and holding rows. + TransactionsEnabled bool `usage:"run the transaction manager, for a capability that writes to the chain"` +} + +// defaultConfig is what the settings are bound to, so an unset flag keeps the +// value here rather than a zero. +var defaultConfig = Config{ + LogPollInterval: *commonconfig.MustNewDuration(time.Second), + FinalityTagEnabled: true, + TransactionsEnabled: true, +} + +// Dependency returns the running chain: the client's pool, a head tracker, a log +// poller and - when this capability writes - a transaction manager. +// +// Everything it is built from is a dependency rather than a default: the client +// says where the chain is, the database says where its state goes, and the +// keystore says who signs. This package chooses none of them, so a binary can +// point them wherever it likes and there is one place per thing to look. +// +// The returned chain is a service: the caller starts it, and starting it is what +// starts polling. +func Dependency( + lggr logger.Logger, + client standalone.BootstrapDependency[evmclient.Client], + db standalone.BootstrapDependency[*sql.DB], + ks standalone.BootstrapDependency[core.Keystore], + account *string, +) standalone.BootstrapDependency[legacyevm.Chain] { + cfg := defaultConfig + // Wrapped so one chain is built however many services resolve this: they are + // meant to share a log poller and a transaction manager, not run one each. + return standalone.OnceBootstrapper[legacyevm.Chain](&dependency{ + lggr: lggr, + client: client, + db: db, + ks: ks, + account: account, + // Held by pointer so the form an embedded instance is built from decodes into + // the same settings the flags were bound to rather than a copy of them. + cfg: &cfg, + }) +} + +type dependency struct { + lggr logger.Logger + client standalone.BootstrapDependency[evmclient.Client] + db standalone.BootstrapDependency[*sql.DB] + ks standalone.BootstrapDependency[core.Keystore] + + // account is which of the keystore's accounts is this chain's. See narrowed. + account *string + + cfg *Config +} + +var _ standalone.BootstrapDependency[legacyevm.Chain] = (*dependency)(nil) + +// Namespace groups these under chain.*, apart from the client's evm.* settings: +// one says where the chain is, the other how this capability follows it. +func (d *dependency) Namespace() string { return "chain" } + +func (d *dependency) Config() any { return d.cfg } + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{d.client, d.db, d.ks} +} + +// ForEmbedding embeds what this is built from: instance i's database, which is a +// schema of its own and so a log poller and a transaction manager of its own, and +// instance i's keys. The chain settings are shared, because the instances of one +// run are nodes on one chain and follow it the same way. +func (d *dependency) ForEmbedding(i, instances int) standalone.BootstrapDependency[legacyevm.Chain] { + embedded := *d + embedded.client = d.client.ForEmbedding(i, instances) + embedded.db = d.db.ForEmbedding(i, instances) + embedded.ks = d.ks.ForEmbedding(i, instances) + return standalone.OnceBootstrapper[legacyevm.Chain](&embedded) +} + +func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (legacyevm.Chain, error) { + client, err := d.client.Get(ctx, cc) + if err != nil { + return nil, fmt.Errorf("failed to get the EVM client: %w", err) + } + + // Which chain this is comes from the client rather than from a setting of its + // own: the client was configured with it, and a second place to say it is a + // second place for it to be wrong. + chainID := client.ConfiguredChainID() + if chainID == nil || chainID.Sign() <= 0 { + return nil, fmt.Errorf("the EVM client is configured for chain %v, which is not a chain ID", chainID) + } + + database, err := d.db.Get(ctx, cc) + if err != nil { + return nil, fmt.Errorf("failed to get the database: %w", err) + } + keystore, err := d.ks.Get(ctx, cc) + if err != nil { + return nil, fmt.Errorf("failed to get the keystore: %w", err) + } + keystore, err = narrowed(ctx, keystore, d.account) + if err != nil { + return nil, err + } + + cfg := d.chainTOML(chainID) + // Validated here rather than left to the chain: a node validates its chains on + // boot and NewTOMLChain trusts that, so this is where that check happens for a + // process with no node around it. + if err := cfg.ValidateConfig(); err != nil { + return nil, fmt.Errorf("invalid configuration for chain %s: %w", chainID, err) + } + + chain, err := legacyevm.NewTOMLChain(cfg, legacyevm.ChainRelayOpts{ + Logger: d.lggr, + // The node's keys, reached through whatever the caller resolved: what signs a + // transaction is the account the registry knows this node by, and this process + // holds neither it nor a copy. + KeyStore: keys.NewChainStore(keystore, chainID), + ChainOpts: legacyevm.ChainOpts{ + ChainConfigs: toml.EVMConfigs{cfg}, + DatabaseConfig: databaseConfig{}, + FeatureConfig: featureConfig{}, + ListenerConfig: listenerConfig{}, + MailMon: mailbox.NewMonitor("EVM", logger.Named(d.lggr, "Mailbox")), + DS: dataSource(database), + // The client is the resolved dependency, dialled once by whoever opened it. + // dialled wraps it so that starting the chain does not dial it again. + GenEthClient: func(*big.Int) evmclient.Client { return dialled{client} }, + }, + }, nil) + if err != nil { + return nil, fmt.Errorf("failed to create chain %s: %w", chainID, err) + } + + d.lggr.Infow("Built the EVM chain", "chainID", chainID, "transactions", d.cfg.TransactionsEnabled) + return chain, nil +} + +// dataSource is the database as chainlink-evm's ORMs read it. +// +// The column mapping has to be set here because the pool was opened by this +// binary rather than by the module whose queries run over it: sqlx maps a field by +// lowercasing its name, which turns ParentHash into parenthash and finds no such +// column. +func dataSource(db *sql.DB) sqlutil.DataSource { + ds := sqlx.NewDb(db, "pgx") + ds.MapperFunc(reflectx.CamelToSnakeASCII) + return ds +} + +// chainTOML is the chain configuration chainlink-evm already knows how to build +// itself, with the handful of things this capability was asked about written into +// it. +// +// It starts from that module's defaults for this chain ID - including whatever is +// specific to the chain in question - so what is configured here is a difference +// from those rather than a second set of values to keep in step with them. +func (d *dependency) chainTOML(chainID *big.Int) *toml.EVMConfig { + id := sqlutil.Big(*chainID) + chain := toml.Defaults(&id) + + chain.LogPollInterval = &d.cfg.LogPollInterval + chain.Transactions.Enabled = &d.cfg.TransactionsEnabled + chain.FinalityTagEnabled = &d.cfg.FinalityTagEnabled + if d.cfg.FinalityDepth > 0 { + chain.FinalityDepth = &d.cfg.FinalityDepth + } + if d.cfg.NoNewHeadsThreshold.Duration() > 0 { + chain.NoNewHeadsThreshold = &d.cfg.NoNewHeadsThreshold + } + + // The log broadcaster is a node's, not this capability's: what watches for logs + // here is the log poller, and the broadcaster is the reason a node insists on a + // websocket per RPC. + broadcaster := false + chain.LogBroadcasterEnabled = &broadcaster + + // Said so that the placeholder node below validates: a primary node needs a + // websocket unless heads are polled over HTTP. It decides nothing, because the + // pool it describes is never built - the client dependency's is - and how that + // one follows heads is its own setting. + chain.NodePool.NewHeadsPollInterval = commonconfig.MustNewDuration(time.Second) + + // One node, standing for the client this chain was given: the pool behind it is + // the client's business, and the chain only needs to see that it has an RPC at + // all. The URL is the loopback placeholder rather than a real one, since nothing + // dials it - see dialled. + name, placeholder := "client", "http://localhost" + sendOnly, loadBalanced, order := false, false, int32(100) + url, err := commonconfig.ParseURL(placeholder) + if err != nil { + // Unreachable: the string above is a constant. + panic(err) + } + + enabled := true + return &toml.EVMConfig{ + ChainID: &id, + Enabled: &enabled, + Chain: chain, + Nodes: toml.EVMNodes{{ + Name: &name, + HTTPURL: url, + SendOnly: &sendOnly, + IsLoadBalancedRPC: &loadBalanced, + Order: &order, + }}, + } +} + +// dialled is a client that has already been dialled, for a chain that would +// otherwise dial it again. +// +// The client is a resolved dependency: whoever opened it owns its connections and +// closes them. A chain built over it still calls Dial when it starts - it expects +// to own the client - and a second dial of the same pool is an error, so this +// answers that call with the yes it already deserves. +type dialled struct { + evmclient.Client +} + +func (dialled) Dial(context.Context) error { return nil } + +// Close is not forwarded either, for the same reason: the pool outlives this +// chain, and closing it here would take it out from under anything else holding +// the same dependency. +func (dialled) Close() {} + +// EVMService is what a capability calls the chain through: the reads, the log +// tracking and the transaction submission a relayer would have handed it. +// +// It is built from a relayer, over the chain above, but that relayer is never +// started: what starting it adds is a node's business - mercury, LLO, the write +// target it configures from its own TOML - and none of it is on the path a +// capability takes. +// +// registry is what the relayer resolves capabilities through, and ks is how it +// chooses which of this node's accounts to send from. +func EVMService(lggr logger.Logger, chain legacyevm.Chain, db *sql.DB, ks core.Keystore, registry core.CapabilitiesRegistry) (commontypes.EVMService, error) { + relayer, err := relay.NewRelayer(lggr, chain, relay.RelayerOpts{ + DS: dataSource(db), + CSAKeystore: unusedKeystore{}, + EVMKeystore: keys.NewChainStore(ks, chain.ID()), + CapabilitiesRegistry: registry, + }) + if err != nil { + return nil, fmt.Errorf("failed to create the EVM service for chain %s: %w", chain.ID(), err) + } + return relayer.EVM() +} + +// unusedKeystore stands in for the CSA keys a relayer wants for mercury, which is +// a path a capability never takes. It is refused rather than left nil because nil +// is what the relayer checks for, and an error says which of the two this is: not +// forgotten, not available. +type unusedKeystore struct{} + +var _ core.Keystore = unusedKeystore{} + +func (unusedKeystore) Accounts(context.Context) ([]string, error) { + return nil, errors.New("this capability holds no CSA keys: they are a node's, for mercury, which a capability does not serve") +} + +func (unusedKeystore) Sign(context.Context, string, []byte) ([]byte, error) { + return nil, errors.New("this capability holds no CSA keys: they are a node's, for mercury, which a capability does not serve") +} + +func (unusedKeystore) Decrypt(context.Context, string, []byte) ([]byte, error) { + return nil, errors.New("this capability holds no CSA keys: they are a node's, for mercury, which a capability does not serve") +} + +// databaseConfig, featureConfig and listenerConfig are the three small +// configurations a chain takes that are the process's rather than the chain's. +// +// A node reads them from its own configuration file, where they are shared by +// everything it runs. Here there is nothing to share them with, so they are the +// values that configuration defaults to. +type databaseConfig struct{} + +func (databaseConfig) DefaultQueryTimeout() time.Duration { return 10 * time.Second } + +func (databaseConfig) LogSQL() bool { return false } + +type featureConfig struct{} + +// LogPoller is always on: a chain capability that watches for logs is the reason +// this exists, and the poller is what watches. +func (featureConfig) LogPoller() bool { return true } + +type listenerConfig struct{} + +// FallbackPollInterval is how often the transaction manager checks for work it was +// not woken for. A node defaults this to a minute; nothing here is different. +func (listenerConfig) FallbackPollInterval() time.Duration { return time.Minute } diff --git a/chain_capabilities/evm/chain/db.go b/chain_capabilities/evm/chain/db.go new file mode 100644 index 000000000..e3cf60639 --- /dev/null +++ b/chain_capabilities/evm/chain/db.go @@ -0,0 +1,391 @@ +// Package chain builds the EVM chain this capability reads and writes: an RPC +// client, a head tracker, a log poller and a transaction manager, assembled from +// chainlink-evm's own components rather than reached through a relayer in the +// node's process. +// +// It lives here rather than in chainlink-evm because it is this binary's way of +// starting up, not that module's: what it does is resolve bootstrap dependencies +// - a client, a database, keys - and hand chainlink-evm what it already knows how +// to take. +package chain + +import ( + "context" + "database/sql" + "database/sql/driver" + "fmt" + "hash/fnv" + "io/fs" + "regexp" + "strings" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/stdlib" + "github.com/pressly/goose/v3" + goosedb "github.com/pressly/goose/v3/database" + gooselock "github.com/pressly/goose/v3/lock" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// defaultSchema is where this capability keeps its chain state, and the schema a +// node's own migrations create for it (0303_create_cre_standalone_schemas.sql). +// +// It is not "evm", which is the node's: a node's own log poller and transaction +// manager live there, and two of either over one set of tables would each treat +// the other's rows as its own. Owning a schema instead means this capability can +// run beside a node, beside another instance of itself, or on a database of its +// own, without any of them agreeing about anything first. +// +// One schema holds every chain, not one per chain: chainlink-evm's tables all +// carry an evm_chain_id, which is how a node keeps every chain it runs in its own +// single evm schema. Processes started together therefore migrate the same schema +// at the same time, which is what the lock in migrate is for. +const defaultSchema = "evm_capability" + +// DBConfig is the database this capability keeps its chain state in. +type DBConfig struct { + URL string `validate:"required" usage:"database url" example:"'postgresql://user:password@localhost:5432/chainlink?sslmode=disable'"` + + // Schema is which schema in it is this capability's. Whatever it is called, the + // tables in it are the ones chainlink-evm's queries name in the evm schema: see + // qualified. + Schema string `usage:"database schema this capability's chain state lives in; must not be the node's own evm schema"` +} + +// DBDependency returns the database this capability's chain state lives in: +// opened, in a schema of its own, and migrated. +// +// The migrations are this binary's, embedded by the caller, and they are written +// against the evm schema like everything else here - so they land wherever the +// configured schema is, for the same reason the queries do. +func DBDependency(lggr logger.Logger, migrations fs.FS, migrationTable string) standalone.BootstrapDependency[*sql.DB] { + // Wrapped so one pool is opened and migrated however many services resolve this. + return standalone.OnceBootstrapper[*sql.DB](&dbDependency{ + lggr: lggr, + migrations: migrations, + migrationTable: migrationTable, + cfg: &DBConfig{Schema: defaultSchema}, + }) +} + +type dbDependency struct { + lggr logger.Logger + migrations fs.FS + migrationTable string + cfg *DBConfig + + // instance names this instance of an embedded run, appended to the schema so that + // instances keep their chain state apart. Empty for a single run. + instance string +} + +var _ standalone.BootstrapDependency[*sql.DB] = (*dbDependency)(nil) + +// Namespace groups the settings under database.*, the names every binary in this +// framework gives them. +func (d *dbDependency) Namespace() string { return "database" } + +func (d *dbDependency) Config() any { return d.cfg } + +func (d *dbDependency) Dependencies() []standalone.BootstrapCommand { return nil } + +// ForEmbedding gives instance i a schema of its own, since the instances of an +// embedded run are separate nodes: they must no more share a log poller's blocks +// than they share a peer identity. +// +// The configured schema is kept as the stem, so a run's schemas sit together and +// an operator can see which run they belong to. +func (d *dbDependency) ForEmbedding(i, _ int) standalone.BootstrapDependency[*sql.DB] { + embedded := *d + embedded.instance = fmt.Sprintf("_node_%d", i) + return standalone.OnceBootstrapper[*sql.DB](&embedded) +} + +func (d *dbDependency) Get(ctx context.Context, _ standalone.CommonConfig) (*sql.DB, error) { + schema := d.schema() + if !identifier.MatchString(schema) { + return nil, fmt.Errorf("invalid --database.schema %q: expected a lowercase identifier", schema) + } + if schema == chainlinkEVMSchema { + return nil, fmt.Errorf("--database.schema must not be %q: that schema is the node's, and this capability's log poller and transaction manager would run over its tables", chainlinkEVMSchema) + } + + db, err := d.open(schema) + if err != nil { + return nil, err + } + + // Usually already there, created by the migrations of whoever owns the database. + // This is for the other cases - a database of this capability's own, and the + // per-instance schemas of an embedded run, which no migration knows about. + if _, err := db.ExecContext(ctx, `CREATE SCHEMA IF NOT EXISTS `+quoteIdentifier(schema)); err != nil { + return nil, fmt.Errorf("failed to create schema %s: %w", schema, err) + } + + if err := migrate(ctx, db, d.migrations, d.migrationTable, schema); err != nil { + return nil, err + } + + d.lggr.Infow("Opened the capability's database", "schema", schema) + return db, nil +} + +// schema is this instance's schema: the configured one, plus which instance of an +// embedded run this is. +func (d *dbDependency) schema() string { + schema := d.cfg.Schema + if schema == "" { + schema = defaultSchema + } + return schema + d.instance +} + +// open opens the pool, with every query rewritten into schema and unqualified +// names resolving there too. +// +// The search path covers what this binary writes itself - the migration history, +// and its own tables - and the rewriting covers what chainlink-evm writes, which +// names its schema in every query. +func (d *dbDependency) open(schema string) (*sql.DB, error) { + config, err := pgx.ParseConfig(d.cfg.URL) + if err != nil { + return nil, fmt.Errorf("failed to parse the database url: %w", err) + } + if config.RuntimeParams == nil { + config.RuntimeParams = map[string]string{} + } + // public stays behind it: extensions and anything else shared by the database + // live there, and an operator's own search path is preserved the same way. + searchPath := schema + ",public" + if existing := config.RuntimeParams["search_path"]; existing != "" { + searchPath = schema + "," + existing + } + config.RuntimeParams["search_path"] = searchPath + + return sql.OpenDB(&connector{ + Connector: stdlib.GetConnector(*config), + rewrite: rewriteInto(schema), + }), nil +} + +// chainlinkEVMSchema is the schema chainlink-evm's queries are written against, +// and the one a node keeps its own chain state in. +const chainlinkEVMSchema = "evm" + +// qualified matches that schema wherever a query names it: `evm.` followed by the +// table, function or type it qualifies. The leading boundary keeps it from +// matching the tail of a longer identifier, so a column called something_evm.x - +// were there one - is left alone. +var qualified = regexp.MustCompile(`\bevm\.`) + +// identifier is what a schema may be called. The name is spliced into SQL rather +// than bound as a parameter, so it is checked rather than escaped: a schema +// needing quotes is a sign of a caller doing something other than naming one. +var identifier = regexp.MustCompile(`^[a-z_][a-z0-9_]*$`) + +// rewriteInto returns the rewrite applied to every query on this pool: the evm +// schema chainlink-evm names becomes the schema this capability owns. +// +// This is what lets a capability use another module's ORM without that module +// having a schema parameter in several hundred query strings, and without this +// one sharing a table with whatever else is on the database. It is not a parser +// and does not pretend to be one; it moves a schema qualifier and nothing else. +func rewriteInto(schema string) func(string) string { + if schema == chainlinkEVMSchema { + return nil + } + replacement := schema + "." + return func(query string) string { + return qualified.ReplaceAllLiteralString(query, replacement) + } +} + +func quoteIdentifier(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +} + +// migrate applies the embedded migrations, tracking them in a table named for +// this binary so that a database holding more than one binary's tables keeps +// their histories apart. +func migrate(ctx context.Context, db *sql.DB, migrations fs.FS, table, schema string) error { + migrations, err := fs.Sub(migrations, "migrations") + if err != nil { + return err + } + + store, err := goosedb.NewStore(goose.DialectPostgres, table) + if err != nil { + return fmt.Errorf("failed to create the goose store: %w", err) + } + + // The advisory lock is named for what it protects - this history, in this schema - + // rather than taken on goose's default ID, which every goose user on the database + // shares: a node migrating its own tables has no reason to wait for this. + locker, err := gooselock.NewPostgresSessionLocker(gooselock.WithLockID(lockID(schema, table))) + if err != nil { + return fmt.Errorf("failed to create the migration locker: %w", err) + } + + // Go migrations are registered globally in goose; reset before building the + // provider so repeated calls (a test, or an embedded run's instances) do not + // accumulate duplicates. See https://github.com/pressly/goose/issues/782 + goose.ResetGlobalMigrations() + + // Locked, because the processes sharing this schema start together: a node running + // this capability on two chains runs two of them, both pointed at the same tables, + // both applying this history on boot. goose serialises nothing across processes on + // its own - its provider mutex is one process's - so without this the second one + // creates a table the first is already creating. + provider, err := goose.NewProvider("", db, migrations, + goose.WithStore(store), + goose.WithSessionLocker(locker), + ) + if err != nil { + return fmt.Errorf("failed to create the goose provider: %w", err) + } + if _, err := provider.Up(ctx); err != nil { + return fmt.Errorf("failed to apply migrations: %w", err) + } + return nil +} + +// lockID is the advisory lock these migrations are applied under: one number per +// schema and history, so processes sharing them queue and processes that do not +// are unaffected. +func lockID(schema, table string) int64 { + h := fnv.New64a() + _, _ = h.Write([]byte(schema + "." + table)) + return int64(h.Sum64()) //#nosec G115 - an advisory lock ID is any 64 bits, signed by definition +} + +// connector opens connections whose queries are rewritten. +// +// The rewriting is here, at the driver, rather than over the DataSource above it, +// because a transaction runs its statements on the connection it was begun on: a +// wrapper further up would rewrite the queries it was handed and miss every one +// inside a transaction, which for a transaction manager is most of them. +type connector struct { + driver.Connector + rewrite func(string) string +} + +func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { + conn, err := c.Connector.Connect(ctx) + if err != nil || c.rewrite == nil { + return conn, err + } + return &rewritingConn{Conn: conn, rewrite: c.rewrite}, nil +} + +// rewritingConn is one connection, rewriting the text of everything asked of it. +// +// Every statement reaches a connection as one of these three - prepared, queried +// or executed - whether it was written by hand, built by an ORM, or run inside a +// transaction, so this is the whole surface. +type rewritingConn struct { + driver.Conn + rewrite func(string) string +} + +var ( + _ driver.ConnPrepareContext = (*rewritingConn)(nil) + _ driver.QueryerContext = (*rewritingConn)(nil) + _ driver.ExecerContext = (*rewritingConn)(nil) + _ driver.ConnBeginTx = (*rewritingConn)(nil) +) + +func (c *rewritingConn) Prepare(query string) (driver.Stmt, error) { + return c.Conn.Prepare(c.rewrite(query)) +} + +func (c *rewritingConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { + preparer, ok := c.Conn.(driver.ConnPrepareContext) + if !ok { + return c.Prepare(query) + } + return preparer.PrepareContext(ctx, c.rewrite(query)) +} + +func (c *rewritingConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + queryer, ok := c.Conn.(driver.QueryerContext) + if !ok { + // database/sql falls back to Prepare when a connection cannot query directly, + // and that path is rewritten too. + return nil, driver.ErrSkip + } + return queryer.QueryContext(ctx, c.rewrite(query), args) +} + +func (c *rewritingConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + execer, ok := c.Conn.(driver.ExecerContext) + if !ok { + return nil, driver.ErrSkip + } + return execer.ExecContext(ctx, c.rewrite(query), args) +} + +// BeginTx hands back the underlying transaction as it is: what a transaction +// carries is statements, and those come back through this connection. +func (c *rewritingConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + beginner, ok := c.Conn.(driver.ConnBeginTx) + if !ok { + //nolint:staticcheck // SA1019: the fallback for a driver that predates ConnBeginTx + return c.Conn.Begin() + } + return beginner.BeginTx(ctx, opts) +} + +// The rest of this file is the optional half of driver.Conn, forwarded. +// +// database/sql asks a connection what it can do by type-asserting, so anything a +// wrapper does not implement is a thing the wrapped connection is taken not to do. +// That is not a lost optimisation: without CheckNamedValue below, database/sql +// converts arguments with its own rules instead of the driver's, and rejects +// everything the driver would have handled - a query passing an array of +// addresses, say, which is how the transaction manager asks about several at once. + +var ( + _ driver.NamedValueChecker = (*rewritingConn)(nil) + _ driver.SessionResetter = (*rewritingConn)(nil) + _ driver.Validator = (*rewritingConn)(nil) + _ driver.Pinger = (*rewritingConn)(nil) +) + +// CheckNamedValue lets the driver decide what an argument may be, which for pgx +// is a good deal more than database/sql's default converter allows. +func (c *rewritingConn) CheckNamedValue(value *driver.NamedValue) error { + checker, ok := c.Conn.(driver.NamedValueChecker) + if !ok { + return driver.ErrSkip + } + return checker.CheckNamedValue(value) +} + +// ResetSession is how a pooled connection is made ready for its next user. +func (c *rewritingConn) ResetSession(ctx context.Context) error { + resetter, ok := c.Conn.(driver.SessionResetter) + if !ok { + return nil + } + return resetter.ResetSession(ctx) +} + +// IsValid is how a connection says it should not be reused. Answering yes for a +// connection that says no would put a broken one back in the pool. +func (c *rewritingConn) IsValid() bool { + validator, ok := c.Conn.(driver.Validator) + if !ok { + return true + } + return validator.IsValid() +} + +func (c *rewritingConn) Ping(ctx context.Context) error { + pinger, ok := c.Conn.(driver.Pinger) + if !ok { + return nil + } + return pinger.Ping(ctx) +} diff --git a/chain_capabilities/evm/chain/db_test.go b/chain_capabilities/evm/chain/db_test.go new file mode 100644 index 000000000..9e2ba3c20 --- /dev/null +++ b/chain_capabilities/evm/chain/db_test.go @@ -0,0 +1,134 @@ +package chain + +import ( + "context" + "database/sql" + "os" + "os/exec" + "sync" + "testing" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// dbURL is the database these tests run against, and whether they run at all: +// what they are about is what two connections to a real one do to each other. +const dbURL = "CL_DATABASE_URL" + +// childSchema is set on the subprocesses TestMigrateIsConcurrent starts, naming +// the schema they are to migrate. It is what tells one of these test binaries it +// is a child rather than the test. +const childSchema = "TEST_MIGRATE_SCHEMA" + +// childStart is when those subprocesses are to begin, so that they begin +// together: what is being tested happens in the moment two of them read an +// unmigrated schema, and processes left to start when they finish loading miss it +// most times out of ten. +const childStart = "TEST_MIGRATE_START" + +// TestMigrateIsConcurrent covers the case a node running this capability on two +// chains creates: two processes, one schema, both migrating it at the same time. +// +// Separate processes rather than goroutines, because that is the whole question - +// goose serialises migrations within a process on its own, so goroutines would +// pass with or without the advisory lock the code takes. +func TestMigrateIsConcurrent(t *testing.T) { + url := os.Getenv(dbURL) + if url == "" { + t.Skip("set " + dbURL + " to run this against a database") + } + if os.Getenv(childSchema) != "" { + t.Skip("this process is a child of the test, see TestMigrateChild") + } + + const schema = "evm_capability_concurrent_test" + + admin, err := sql.Open("pgx", url) + require.NoError(t, err) + t.Cleanup(func() { _ = admin.Close() }) + + drop := `DROP SCHEMA IF EXISTS ` + quoteIdentifier(schema) + ` CASCADE` + _, err = admin.ExecContext(t.Context(), drop) + require.NoError(t, err) + _, err = admin.ExecContext(t.Context(), `CREATE SCHEMA `+quoteIdentifier(schema)) + require.NoError(t, err) + t.Cleanup(func() { + _, _ = admin.ExecContext(context.WithoutCancel(t.Context()), drop) + }) + + // Far enough out that every child is loaded and waiting before any of them moves. + start := time.Now().Add(2 * time.Second) + + const processes = 4 + output := make([]string, processes) + errs := make([]error, processes) + var wg sync.WaitGroup + for i := range processes { + wg.Add(1) + go func() { + defer wg.Done() + + cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=TestMigrateChild", "-test.v") + cmd.Env = append(os.Environ(), + childSchema+"="+schema, + childStart+"="+start.Format(time.RFC3339Nano), + ) + out, err := cmd.CombinedOutput() + output[i], errs[i] = string(out), err + }() + } + wg.Wait() + + for i, err := range errs { + require.NoErrorf(t, err, "process %d: %s", i, output[i]) + } + + // Applied once and only once: whichever process won, the others found the history + // already at that version rather than applying it again. + var version int + require.NoError(t, admin.QueryRowContext(t.Context(), + `SELECT max(version_id) FROM `+quoteIdentifier(schema)+`.`+migrationTable).Scan(&version)) + require.Positive(t, version) + + var tables int + require.NoError(t, admin.QueryRowContext(t.Context(), + `SELECT count(*) FROM information_schema.tables WHERE table_schema = $1 AND table_name = 'trigger_pending_events'`, + schema).Scan(&tables)) + require.Equal(t, 1, tables) +} + +// migrationTable is what the binary calls its goose history; the name matters +// only in that every process here agrees on it. +const migrationTable = "evm_capability_migrations" + +// TestMigrateChild is one of the processes TestMigrateIsConcurrent starts: it +// migrates the schema it was given and says whether that worked. It is a test +// only because that is how this binary is run. +func TestMigrateChild(t *testing.T) { + url, schema := os.Getenv(dbURL), os.Getenv(childSchema) + if url == "" || schema == "" { + t.Skip("started directly rather than by TestMigrateIsConcurrent") + } + + if at := os.Getenv(childStart); at != "" { + start, err := time.Parse(time.RFC3339Nano, at) + require.NoError(t, err) + time.Sleep(time.Until(start)) + } + + d := &dbDependency{ + lggr: logger.Test(t), + migrations: os.DirFS(".."), + migrationTable: migrationTable, + cfg: &DBConfig{URL: url, Schema: schema}, + } + db, err := d.open(schema) + require.NoError(t, err) + t.Cleanup(func() { _ = db.Close() }) + + require.NoError(t, migrate(t.Context(), db, d.migrations, migrationTable, schema)) +} diff --git a/chain_capabilities/evm/chain/keys.go b/chain_capabilities/evm/chain/keys.go new file mode 100644 index 000000000..399869605 --- /dev/null +++ b/chain_capabilities/evm/chain/keys.go @@ -0,0 +1,112 @@ +package chain + +import ( + "context" + "crypto/ecdsa" + "crypto/sha256" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// deterministicSeedPrefix domain-separates these keys from anything else derived +// from an instance index. Changing it changes every derived address. +const deterministicSeedPrefix = "cre/standalone/instance/chain/evm/" + +// DeterministicKeystore is the keystore instance i of a multi-instance local run +// signs with: one key, derived from the index rather than borrowed from a node. +// +// A process running beside a node signs with the node's keys, and an embedded run +// has no node - that is what embedding means. Deriving rather than generating is +// what makes it usable: the addresses are known before anything starts, so +// whatever has to fund them, or list them as transmitters, can be set up from the +// instance count alone (see DeterministicAddress). +// +// These keys are public by construction. Nothing derived this way is a secret, +// and nothing that matters may be protected by one: embed is for local runs and +// tests. +func DeterministicKeystore(instance int) (core.Keystore, error) { + key, err := DeterministicKey(instance) + if err != nil { + return nil, err + } + return &localKeystore{address: crypto.PubkeyToAddress(key.PublicKey), key: key}, nil +} + +// KeystoreFromPrivateKey is the keystore an embedded run signs with when it is +// given a key rather than left to derive one. +// +// An embedded run has no node to borrow keys from, so it derives them - which is +// what makes a local run reproducible, and what makes its accounts unfunded +// everywhere that matters. A run pointed at a real chain needs an account that +// exists on it, and this is where that account comes from. +// +// The key is a secret this process then holds, which is the thing every other +// part of this design avoids. That is the trade embedding already makes: it is +// for local runs and tests, and a node beside a real deployment signs through +// crecore and never sees a key at all. +func KeystoreFromPrivateKey(hexKey string) (core.Keystore, error) { + key, err := crypto.HexToECDSA(strings.TrimPrefix(strings.TrimSpace(hexKey), "0x")) + if err != nil { + return nil, fmt.Errorf("failed to read the configured private key: %w", err) + } + return &localKeystore{address: crypto.PubkeyToAddress(key.PublicKey), key: key}, nil +} + +// DeterministicKey returns the chain key of instance i. +func DeterministicKey(instance int) (*ecdsa.PrivateKey, error) { + seed := sha256.Sum256([]byte(deterministicSeedPrefix + strconv.Itoa(instance))) + key, err := crypto.ToECDSA(seed[:]) + if err != nil { + return nil, fmt.Errorf("failed to derive the chain key of instance %d: %w", instance, err) + } + return key, nil +} + +// DeterministicAddress returns the account DeterministicKey gives instance i, so +// a caller preparing a run - funding the accounts, naming them as transmitters - +// can do it without starting anything. +func DeterministicAddress(instance int) (common.Address, error) { + key, err := DeterministicKey(instance) + if err != nil { + return common.Address{}, err + } + return crypto.PubkeyToAddress(key.PublicKey), nil +} + +// localKeystore is the one derived key, as core.Keystore. +type localKeystore struct { + address common.Address + key *ecdsa.PrivateKey +} + +var _ core.Keystore = (*localKeystore)(nil) + +func (k *localKeystore) Accounts(context.Context) ([]string, error) { + return []string{k.address.Hex()}, nil +} + +// Sign signs the digest it is given, and only for the account it holds: an +// instance asked to sign as another instance is a run that has confused its +// members for each other. +func (k *localKeystore) Sign(_ context.Context, account string, data []byte) ([]byte, error) { + if !strings.EqualFold(account, k.address.Hex()) { + return nil, fmt.Errorf("this instance signs as %s, not %s", k.address, account) + } + if len(data) == 0 { + // The existence check core.Keystore describes, which answers with no signature + // rather than a signature of nothing. + return nil, nil + } + return crypto.Sign(data, k.key) +} + +func (k *localKeystore) Decrypt(context.Context, string, []byte) ([]byte, error) { + return nil, errors.New("chain keys sign; they do not decrypt") +} diff --git a/chain_capabilities/evm/chain/keys_test.go b/chain_capabilities/evm/chain/keys_test.go new file mode 100644 index 000000000..3cd087e72 --- /dev/null +++ b/chain_capabilities/evm/chain/keys_test.go @@ -0,0 +1,45 @@ +package chain + +import ( + "encoding/hex" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestKeystoreFromPrivateKey covers the account an embedded run pointed at a real +// chain sends from: the one the configured key belongs to, whether or not it was +// written with the 0x a wallet exports it with. +func TestKeystoreFromPrivateKey(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + + raw := hex.EncodeToString(crypto.FromECDSA(key)) + want := crypto.PubkeyToAddress(key.PublicKey).Hex() + + for _, written := range []string{raw, "0x" + raw, " " + raw + "\n"} { + ks, err := KeystoreFromPrivateKey(written) + require.NoError(t, err) + + accounts, err := ks.Accounts(t.Context()) + require.NoError(t, err) + assert.Equal(t, []string{want}, accounts) + + // Signed as that account, and recovered to it: a key read wrongly would still + // produce a signature, just not this node's. + digest := crypto.Keccak256([]byte("a transaction")) + signature, err := ks.Sign(t.Context(), want, digest) + require.NoError(t, err) + + recovered, err := crypto.SigToPub(digest, signature) + require.NoError(t, err) + assert.Equal(t, want, crypto.PubkeyToAddress(*recovered).Hex()) + } +} + +func TestKeystoreFromPrivateKeyRejectsNonsense(t *testing.T) { + _, err := KeystoreFromPrivateKey("not a key") + require.ErrorContains(t, err, "failed to read the configured private key") +} diff --git a/chain_capabilities/evm/config/config.go b/chain_capabilities/evm/config/config.go index 4e898a8ed..293c5cbcd 100644 --- a/chain_capabilities/evm/config/config.go +++ b/chain_capabilities/evm/config/config.go @@ -1,23 +1,108 @@ +// Package config is what the EVM capability needs told, as opposed to what it +// reads from the chain or is handed by the process hosting it. +// +// The chain's own settings are not here. Where the RPC is, how deep finality is +// and how often the log poller reads are the chain's, and they are configured +// where the chain is built (chainlink-evm's cre/evmchain) so that one set of +// evm.* settings describes one chain rather than two halves of it. package config -import "time" +import ( + "errors" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" +) type Config struct { - ChainID uint64 `json:"chainId"` - Network string `json:"network"` - LogTriggerPollInterval time.Duration `json:"logTriggerPollInterval"` - LogTriggerSendChannelBufferSize uint64 `json:"logTriggerSendChannelBufferSize"` - LogTriggerLimitQueryLogSize uint64 `json:"logTriggerLimitQueryLogSize"` - CREForwarderAddress string `json:"creForwarderAddress"` - ForwarderLookbackBlocks int64 `json:"forwarderLookbackBlocks"` // defines how many blocks back to search for the ReportProcessed event (default 100). - // The minimum amount of gas that the receiver contract must get to process the forwarder report. This is the default value used when the user doesn't specify a gas limit when invoking WriteReport. - ReceiverGasMinimum uint64 `json:"receiverGasMinimum"` - NodeAddress string `json:"nodeAddress"` - ObservationPollerWorkersCount uint `json:"observationPollerWorkersCount"` - ObservationPollPeriod time.Duration `json:"observationPollPeriod"` - ChainHeightPollPeriod time.Duration `json:"chainHeightPollPeriod"` - UnknownRequestsTTL time.Duration `json:"unknownRequestsTTL"` - // DeltaStage for staggered transmission scheduling - DeltaStage time.Duration `json:"deltaStage"` - IsLocaL bool `json:"isLocal"` // use in integration-test to skip transmission scheduler initialization for log_trigger + // CREForwarderAddress is the contract a report is written through, and + // ForwarderLookbackBlocks is how far back its ReportProcessed events are read + // when this process has to work out whether a report already landed. + // Neither is `validate:"required"`, which is checked when the configuration is + // decoded: an embedded run starting a chain of its own has no forwarder to name + // until it has deployed one. Both are still required to run - see Validate, which + // says so after the chain, if there is going to be one, exists. + CREForwarderAddress string `json:"creForwarderAddress" usage:"address of the CRE forwarder contract reports are written through; defaulted to the one deployed on a simulated chain" example:"'0x0000000000000000000000000000000000000000'"` + ForwarderLookbackBlocks int64 `json:"forwarderLookbackBlocks" usage:"how many blocks back to search for the forwarder's ReportProcessed event"` + + // ReceiverGasMinimum is the gas a receiving contract is guaranteed for + // processing a report, used when a workflow names no limit of its own. + ReceiverGasMinimum uint64 `json:"receiverGasMinimum" usage:"gas a receiver contract is guaranteed when a workflow names no limit of its own"` + + // NodeAddress is the account this node sends this chain's transactions from, + // which the process holding the keys signs for, and the account the telemetry + // this capability emits is reported under. + // + // It is also what narrows the keystore to this chain: the node holds a key per + // chain it runs, and the store they share cannot say which is which. See + // chain.OnlyAccount. + NodeAddress string `json:"nodeAddress" usage:"the account this node sends this chain's transactions from; the node must hold its key" example:"'0x0000000000000000000000000000000000000000'"` + + // PrivateKeys are the keys an embedded run signs this chain's transactions with, + // one per instance, in instance order. Empty - the ordinary case - leaves each + // instance deriving its own from its index (see chain.DeterministicKeystore), and + // a process running beside a node ignores this entirely: its keys are the node's, + // reached through --keystore.proxy-address. + // + // It exists for pointing an embedded run at a real chain, where a derived account + // is an account with no funds. It is a secret, so prefer CRE_EVM_PRIVATE_KEYS to a + // flag, which every process on the machine can read. + PrivateKeys []string `json:"privateKeys" usage:"private keys an embedded run signs with, one per instance; prefer the CRE_EVM_PRIVATE_KEYS env var to a flag" flagdocs:"noexample"` + + // LogTrigger* bound the log trigger: how often it reads, how much it will hold + // for a workflow that is not keeping up, and how many logs one query returns. + LogTriggerPollInterval time.Duration `json:"logTriggerPollInterval" usage:"how often a registered log trigger reads the logs it matched"` + LogTriggerSendChannelBufferSize uint64 `json:"logTriggerSendChannelBufferSize" usage:"how many matched logs are held for a workflow that is not keeping up"` + LogTriggerLimitQueryLogSize uint64 `json:"logTriggerLimitQueryLogSize" usage:"how many logs one query returns; must not exceed the send buffer"` + + // Observation* configure the poll that answers another node's request for what + // this node saw, which is what the chain consensus round is made of. + ObservationPollerWorkersCount uint `json:"observationPollerWorkersCount" usage:"how many requests for this node's observation are answered at once"` + ObservationPollPeriod time.Duration `json:"observationPollPeriod" usage:"how often a pending request is re-checked for an answer"` + + // ChainHeightPollPeriod is how often this node reads the chain's height, which + // is what its nodes agree on before answering a read. + ChainHeightPollPeriod time.Duration `json:"chainHeightPollPeriod" usage:"how often the chain's height is read, for the height the DON agrees on"` + + // UnknownRequestsTTL is how long a request that arrived before this node knew + // about it is kept, so a round is not lost to the order messages arrive in. + UnknownRequestsTTL time.Duration `json:"unknownRequestsTTL" usage:"how long a request this node has not seen locally is kept before it is dropped"` + + // DeltaStage staggers transmission across the DON, so one agreed report is not + // sent by every member at once. Zero disables it. + DeltaStage time.Duration `json:"deltaStage" usage:"delay between DON members transmitting the same report; 0 sends without staggering"` + + // IsLocal skips the DON lookup that staggering needs, for a test running this + // capability without a registry behind it. + IsLocal bool `json:"isLocal" usage:"skip the DON lookup transmission scheduling needs, for local runs"` +} + +// Default is what a setting keeps when it is not configured. The values that are +// zero here have no useful default - an address, a gas floor - and are required. +var Default = Config{ + ForwarderLookbackBlocks: 100, + LogTriggerPollInterval: time.Second, + ObservationPollerWorkersCount: 10, + ObservationPollPeriod: 2 * time.Second, + ChainHeightPollPeriod: time.Second, + UnknownRequestsTTL: 10 * time.Second, +} + +// Validate rejects what the capability cannot run with, so a misconfiguration is +// a startup error rather than the first request failing. +func (c Config) Validate() error { + var errs []error + + if !common.IsHexAddress(c.CREForwarderAddress) { + errs = append(errs, fmt.Errorf("--evm.cre-forwarder-address %q is not an address", c.CREForwarderAddress)) + } + if c.ReceiverGasMinimum == 0 { + errs = append(errs, errors.New("--evm.receiver-gas-minimum must be greater than 0")) + } + if c.LogTriggerPollInterval < 0 { + errs = append(errs, fmt.Errorf("--evm.log-trigger-poll-interval must not be negative, got %s", c.LogTriggerPollInterval)) + } + + return errors.Join(errs...) } diff --git a/chain_capabilities/evm/go.mod b/chain_capabilities/evm/go.mod index 4261bc829..799b5c1d5 100644 --- a/chain_capabilities/evm/go.mod +++ b/chain_capabilities/evm/go.mod @@ -3,200 +3,242 @@ module github.com/smartcontractkit/capabilities/chain_capabilities/evm go 1.26.2 require ( - github.com/ethereum/go-ethereum v1.17.0 + github.com/ethereum/go-ethereum v1.17.3 github.com/google/go-cmp v0.7.0 + github.com/jackc/pgx/v5 v5.9.2 + github.com/pressly/goose/v3 v3.27.1 github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260615195421-fb87220e503f github.com/smartcontractkit/capabilities/libs v0.0.0-20260609124022-2749e4a32bfb - github.com/smartcontractkit/chain-selectors v1.0.103 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260601211238-9f526774fef0 + github.com/smartcontractkit/chain-selectors v1.0.104 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260410162948-2dca02f24e98 - github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1 - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 + 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.uber.org/zap v1.27.1 + go.uber.org/zap v1.28.0 google.golang.org/protobuf v1.36.11 ) require ( - github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect + filippo.io/edwards25519 v1.2.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/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // 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/apache/arrow-go/v18 v18.3.1 // indirect - github.com/bits-and-blooms/bitset v1.24.0 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/apache/arrow-go/v18 v18.6.0 // indirect + github.com/avast/retry-go/v4 v4.7.0 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v5 v5.0.3 // 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/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 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/consensys/gnark-crypto v0.19.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect - github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/cockroachdb/redact v1.1.6 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect + github.com/consensys/gnark-crypto v0.20.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/dchest/siphash v1.2.3 // indirect - github.com/deckarep/golang-set/v2 v2.6.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/deckarep/golang-set/v2 v2.9.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dominikbraun/graph v0.23.0 // indirect github.com/emicklei/dot v1.6.2 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/esote/minmaxheap v1.0.0 // 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/expr-lang/expr v1.17.8 // indirect + github.com/fbsobreira/gotron-sdk v0.0.0-20250403083053-2943ce8c759b // indirect github.com/ferranbt/fastssz v0.1.4 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect - github.com/getsentry/sentry-go v0.27.0 // indirect - github.com/go-errors/errors v1.5.1 // 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/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-json v0.10.6 // 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/snappy v1.0.0 // indirect - github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/google/btree v1.1.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/grafana/pyroscope-go v1.3.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect github.com/hashicorp/go-bexpr v0.1.10 // 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/jackc/pgx/v5 v5.9.2 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // 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/jonboulle/clockwork v0.5.0 // indirect - github.com/klauspost/compress v1.18.2 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // 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/marcboeker/go-duckdb v1.8.5 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/logging v0.2.2 // 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.1 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/smartcontractkit/chainlink-common/keystore v1.1.1-0.20260529092756-a94bc8ce96d6 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0 // indirect - github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57 // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260401162955-be2bc6b5264b // 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/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect + github.com/smartcontractkit/chainlink-automation v0.8.1 // 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-data-streams v0.1.15-0.20260522094612-5f9f748bd87a // indirect + github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // 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-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // 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/chainlink-protos/svr v1.3.0 // indirect github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335 // 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/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 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a // 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.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // 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.6 // indirect + github.com/urfave/cli/v2 v2.27.7 // indirect + github.com/wlynxg/anet v0.0.5 // 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/zeebo/xxh3 v1.0.2 // indirect - go.dedis.ch/fixbuf v1.0.3 // indirect - go.dedis.ch/kyber/v3 v3.1.0 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - golang.org/x/mod v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.45.0 // indirect - golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // 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 - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) require ( - github.com/XSAM/otelsql v0.37.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/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/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/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // 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.28.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/uuid v1.6.0 // 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/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/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/jmoiron/sqlx v1.4.0 // indirect + github.com/jmoiron/sqlx v1.4.0 github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.11.1 // 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/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 + github.com/prometheus/client_golang v1.23.2 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/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect - github.com/scylladb/go-reflectx v1.0.1 // indirect + github.com/scylladb/go-reflectx v1.0.1 github.com/shopspring/decimal v1.4.0 // 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/libocr v0.0.0-20260130195252-6e18e2a30acc - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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.38.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.37.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/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect @@ -205,15 +247,29 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.52.0 + golang.org/x/crypto v0.53.0 golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.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 + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.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 ) replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20250528121202-292529af39df + +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 + +replace github.com/smartcontractkit/chainlink-evm => ../../../chainlink-evm + +// 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/chain_capabilities/evm/go.sum b/chain_capabilities/evm/go.sum index abb6c2172..e73109e93 100644 --- a/chain_capabilities/evm/go.sum +++ b/chain_capabilities/evm/go.sum @@ -1,41 +1,47 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/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/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/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.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.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/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/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/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -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/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= -github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +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/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= +github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= +github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= 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/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/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/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/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 v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 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= @@ -47,34 +53,39 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR 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/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-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= -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/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= -github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +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/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/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -83,50 +94,68 @@ 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/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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.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/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/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/esote/minmaxheap v1.0.0 h1:rgA7StnXXpZG6qlM0S7pUmEv1KpWe32rYT4x8J8ntaA= +github.com/esote/minmaxheap v1.0.0/go.mod h1:Ln8+i7fS1k3PLgZI2JAo0iA1as95QnIYiGCrqSJ5FZk= +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/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= +github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= 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/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +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.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +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/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/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-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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -144,16 +173,17 @@ 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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +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/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= @@ -180,15 +210,16 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 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/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +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.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= @@ -200,22 +231,22 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 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/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/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/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-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-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -224,6 +255,8 @@ github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/ 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/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= @@ -233,15 +266,19 @@ github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXei github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 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/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/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= @@ -300,8 +337,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= @@ -312,13 +351,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -340,10 +376,8 @@ 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.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= -github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -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.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -356,19 +390,17 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -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-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.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= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +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/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 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= @@ -377,12 +409,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 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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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= @@ -398,38 +433,50 @@ 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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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/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/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= 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/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.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= +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 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-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -441,8 +488,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy 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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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/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= @@ -454,64 +501,93 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 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/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.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.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260615195421-fb87220e503f h1:ovzaEXpe8k5Lx7MjQKrwxZZNV6uXGt9xPhtXr87k2Ow= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260615195421-fb87220e503f/go.mod h1:gp/Xrw5nvPswONfr48WKWvAoTTg+Xv/tlk0SRrru8Qw= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260609124022-2749e4a32bfb h1:TskymCV/uP2plDgR2PDqGdavIXnR/rK+wxelqAW5u+s= -github.com/smartcontractkit/capabilities/libs v0.0.0-20260609124022-2749e4a32bfb/go.mod h1:LS7F8U2YZNc0Vt8f6SVWUUigGLxdxZMpyC7VCcUTagg= -github.com/smartcontractkit/chain-selectors v1.0.103 h1:PpvIinn1TIDT7nh/P5KLQunRk0Kp1IR6moP2IGvlP58= -github.com/smartcontractkit/chain-selectors v1.0.103/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260601211238-9f526774fef0 h1:ekpMT6wV+caBWnaBGUD/j1eoal+DhNLq7jv1hFf/nyU= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260601211238-9f526774fef0/go.mod h1:6jgqiFXFJHqjkvFFmuf8gvoUFa6Ygx/D1tKnIL+CCF8= -github.com/smartcontractkit/chainlink-common/keystore v1.1.1-0.20260529092756-a94bc8ce96d6 h1:fWsYxxj35fp1/6YZngoTsOTMLqDie4N5X0osAOdhUTE= -github.com/smartcontractkit/chainlink-common/keystore v1.1.1-0.20260529092756-a94bc8ce96d6/go.mod h1:6JexOOhPhknQ0QMuppFIlOpm6wCp54yZMxai+tWugwY= -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-evm v0.3.4-0.20260410162948-2dca02f24e98 h1:h/L6wrXYLQalI/vHm6qg/KBv6d7kMb3geMHV5hCM1t4= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260410162948-2dca02f24e98/go.mod h1:6vCMfxz7cMW0wWseNKtct+b1JJbbRVJJhh/t6pQWN3M= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1 h1:NTODgwAil7BLoijS7y6KnEuNbQ9v60VUhIR9FcAzIhg= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1/go.mod h1:oyfOm4k0uqmgZIfxk1elI/59B02shbbJQiiUdPdbMgI= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57 h1:sCrr1Oy/JZstf/Oi2cRuU4mDN1BRUKfXP2CKByCMADg= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260401162955-be2bc6b5264b h1:L1So1EDBDRET3j/TdV1Gjv3qWARoa/NPRaU7k4r30yA= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260401162955-be2bc6b5264b/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a h1:PsFckZp3Dhb5pVc0Xccj1lvnOEg0H3eQdjtZgnCKd+4= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= +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-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= +github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= +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-data-streams v0.1.15-0.20260522094612-5f9f748bd87a h1:8bIqv4r7SgDWkXL2Qz/Ijw+YjZY1uroIte3E2v2keVk= +github.com/smartcontractkit/chainlink-data-streams v0.1.15-0.20260522094612-5f9f748bd87a/go.mod h1:dF5JiHWueHjYguUUUrFeb03MkcDqha/tssEkqTkgzp4= +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/capabilities v0.0.0-20260423135514-5b1a7565a99c h1:AYRSQarVw1EJXUrGvHSwmRTtNHHww/i3xwLat5CshUE= +github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= +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-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/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/svr v1.3.0 h1:vOw+MbXtkPK8l/hA7uLASIgCwv05YzmuoIMuOXPa+g0= +github.com/smartcontractkit/chainlink-protos/svr v1.3.0/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= 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/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335 h1:7bxYNrPpygn8PUSBiEKn8riMd7CXMi/4bjTy0fHhcrY= +github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335/go.mod h1:ccjEgNeqOO+bjPddnL4lUrNLzyCvGCxgBjJdhFX3wa8= +github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20250528121202-292529af39df h1:36e3ROIZyV/qE8SvFOACXtXfMOMd9vG4+zY2v2ScXkI= +github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20250528121202-292529af39df/go.mod h1:4WhGgCA0smBbBud5mK+jnDb2wwndMvoqaWBJ3OV/7Bw= +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-20260130195252-6e18e2a30acc h1:8VJgxHEICd0oETMQhce5kqV75kgpKhbBi0YFeVs74TM= -github.com/smartcontractkit/libocr v0.0.0-20260130195252-6e18e2a30acc/go.mod h1:oJkBKVn8zoBQm7Feah9CiuEHyCqAhnp1LJBzrvloQtM= +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/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/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.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= @@ -525,10 +601,14 @@ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl 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/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/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/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a h1:YuO+afVc3eqrjiCUizNCxI53bl/BnPiVwXqLzqYTqgU= +github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a/go.mod h1:/sfW47zCZp9FrtGcWyo1VjbgDaodxX9ovZvgLb/MxaA= 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= @@ -536,23 +616,31 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT 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.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/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/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.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/umbracle/ethgo v0.1.3 h1:s8D7Rmphnt71zuqrgsGTMS5gTNbueGO1zKLh7qsFzTM= +github.com/umbracle/ethgo v0.1.3/go.mod h1:g9zclCLixH8liBI27Py82klDkW7Oo33AxUOr+M9lzrU= +github.com/umbracle/fastrlp v0.0.0-20220527094140-59d5dd30e722 h1:10Nbw6cACsnQm7r34zlpJky+IzxVLRk6MKTS2d3Vp0E= +github.com/umbracle/fastrlp v0.0.0-20220527094140-59d5dd30e722/go.mod h1:c8J0h9aULj2i3umrfyestM6jCq0LK0U6ly6bWy96nd4= +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/valyala/fastjson v1.4.1 h1:hrltpHpIpkaxll8QltMU8c3QZ5+qIiCL8yKqPFJI/yE= +github.com/valyala/fastjson v1.4.1/go.mod h1:nV6MsjxL2IMJQUoHDIrjEI7oLyeqK6aBD7EFWPsvP8o= 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/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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -560,47 +648,40 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -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= -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.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= 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.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +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.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +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= @@ -635,11 +716,14 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E 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.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= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -655,8 +739,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= @@ -671,8 +755,6 @@ 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.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= 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= @@ -697,8 +779,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/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= @@ -708,12 +790,11 @@ 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= -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= @@ -748,17 +829,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.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/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= @@ -779,8 +857,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= @@ -802,8 +880,6 @@ 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.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= @@ -811,8 +887,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T 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= @@ -821,17 +895,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= @@ -870,5 +944,13 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh 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= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +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= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/chain_capabilities/evm/height/provider.go b/chain_capabilities/evm/height/provider.go index b70da5737..063289711 100644 --- a/chain_capabilities/evm/height/provider.go +++ b/chain_capabilities/evm/height/provider.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ethereum/go-ethereum/rpc" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" diff --git a/chain_capabilities/evm/internal/contracts/cre_forwarder.go b/chain_capabilities/evm/internal/contracts/cre_forwarder.go index 8c4bccec4..e13686bc3 100644 --- a/chain_capabilities/evm/internal/contracts/cre_forwarder.go +++ b/chain_capabilities/evm/internal/contracts/cre_forwarder.go @@ -16,11 +16,12 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" - evmcap "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/forwarder" workflowpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + + evmcap "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" ) type TransmissionState uint8 diff --git a/chain_capabilities/evm/internal/contracts/cre_forwarder_test.go b/chain_capabilities/evm/internal/contracts/cre_forwarder_test.go index 923b04d03..416915f06 100644 --- a/chain_capabilities/evm/internal/contracts/cre_forwarder_test.go +++ b/chain_capabilities/evm/internal/contracts/cre_forwarder_test.go @@ -13,9 +13,10 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/crypto/sha3" - evmcap "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" workflowpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + evmcap "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" diff --git a/chain_capabilities/evm/internal/contracts/mocks/cre_forwarder_client.go b/chain_capabilities/evm/internal/contracts/mocks/cre_forwarder_client.go index bba4a78b3..e9d70650d 100644 --- a/chain_capabilities/evm/internal/contracts/mocks/cre_forwarder_client.go +++ b/chain_capabilities/evm/internal/contracts/mocks/cre_forwarder_client.go @@ -6,11 +6,13 @@ import ( context "context" common "github.com/ethereum/go-ethereum/common" - contracts "github.com/smartcontractkit/capabilities/chain_capabilities/evm/internal/contracts" - chain_capabilitiesevm "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" + mock "github.com/stretchr/testify/mock" + evm "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" sdk "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" - mock "github.com/stretchr/testify/mock" + + contracts "github.com/smartcontractkit/capabilities/chain_capabilities/evm/internal/contracts" + chain_capabilitiesevm "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" ) // CREForwarderClient is an autogenerated mock type for the CREForwarderClient type diff --git a/chain_capabilities/evm/main.go b/chain_capabilities/evm/main.go index 38faa09d5..14c85656f 100644 --- a/chain_capabilities/evm/main.go +++ b/chain_capabilities/evm/main.go @@ -1,291 +1,200 @@ +// Command evm runs the EVM chain capability as its own binary. +// +// It hosts no node of its own. The chain it reads and writes is built here from +// chainlink-evm's own components - an RPC client, a head tracker, a log poller +// and a transaction manager over a database of its own - rather than reached +// through a relayer in the node's process. What it still borrows from the node is +// what only a node has: the keys it transmits under, the rage networking its +// oracle runs over, and the registry that says which DON it is. package main import ( "context" - "encoding/json" - "errors" + "database/sql" + "embed" "fmt" - "strconv" - "time" + "log" - "github.com/ethereum/go-ethereum/common" - chainselectors "github.com/smartcontractkit/chain-selectors" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/jmoiron/sqlx" + "github.com/spf13/cobra" - caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" - - "github.com/smartcontractkit/capabilities/chain_capabilities/evm/height" - "github.com/smartcontractkit/capabilities/libs/chainconsensus" + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" - consMetrics "github.com/smartcontractkit/capabilities/libs/chainconsensus/metrics" - "github.com/smartcontractkit/capabilities/libs/chainconsensus/oracle" - "github.com/smartcontractkit/capabilities/libs/chainconsensus/poller" + "github.com/smartcontractkit/chainlink-evm/pkg/chains/legacyevm" + creevm "github.com/smartcontractkit/chainlink-evm/pkg/cre/evm" - ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" - "github.com/smartcontractkit/capabilities/chain_capabilities/evm/actions" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/chain" "github.com/smartcontractkit/capabilities/chain_capabilities/evm/config" "github.com/smartcontractkit/capabilities/chain_capabilities/evm/monitoring" - "github.com/smartcontractkit/capabilities/chain_capabilities/evm/trigger" - "github.com/smartcontractkit/capabilities/libs/loopserver" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" - evmcapserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm/server" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/loop" - "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" - "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/simulated" + consMetrics "github.com/smartcontractkit/capabilities/libs/chainconsensus/metrics" + "github.com/smartcontractkit/capabilities/libs/standalone" + "github.com/smartcontractkit/capabilities/libs/standalone/capability" + "github.com/smartcontractkit/capabilities/libs/standalone/eventstore" + standalonegrpc "github.com/smartcontractkit/capabilities/libs/standalone/grpc" + "github.com/smartcontractkit/capabilities/libs/standalone/keystore" + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" ) -const CapabilityName = "evm" +//go:embed migrations/*.sql +var embeddedMigrations embed.FS -type capabilityGRPCService struct { - capabilities.CapabilityInfo - chainSelector uint64 - capability - lggr logger.Logger - limitsFactory limits.Factory -} - -type capability struct { - *actions.EVM - id string - requestPoller *poller.Poller - consensusHandler chainconsensus.Handler - oracle core.Oracle - triggerService *trigger.LogTriggerService - heightProvider *height.Provider -} - -var _ evmcapserver.ClientCapability = &capabilityGRPCService{} +// migrationsTable is this binary's goose history. Named for the binary rather +// than shared, so that a database holding more than one capability's tables keeps +// their migrations apart. +const migrationsTable = "evm_capability_migrations" func main() { - loopserver.ServeNew(CapabilityName, func(s *loop.Server) loop.StandardCapabilities { - return evmcapserver.NewClientServer(&capabilityGRPCService{lggr: s.Logger, limitsFactory: s.LimitsFactory}) - }, loop.WithOtelViews(append(consMetrics.MetricViews(), monitoring.MetricViews()...))) -} - -func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies core.StandardCapabilitiesDependencies) error { - c.lggr.Infof("Initialising %s", CapabilityName) - - cfg, err := c.unmarshalConfig(dependencies.Config) - if err != nil { - return fmt.Errorf("failed to unmarshal config: %w", err) - } - - c.lggr.Infof("Initialising %s, ChainId: %d, Network: %s", CapabilityName, cfg.ChainID, cfg.Network) - - metrics, err := monitoring.NewMetrics() - if err != nil { - return fmt.Errorf("failed to create metrics: %w", err) - } - - processor, err := monitoring.NewProcessor(c.lggr, metrics) - if err != nil { - return fmt.Errorf("failed to create monitoring proto processor: %w", err) - } - - relayID := types.NewRelayID(cfg.Network, fmt.Sprintf("%d", cfg.ChainID)) - relayer, err := dependencies.RelayerSet.Get(ctx, relayID) - if err != nil { - return fmt.Errorf("failed to fetch relayer for chainID %d from relayerSet: %w", cfg.ChainID, err) - } - - cs, ok := chainselectors.EvmChainIdToChainSelector()[cfg.ChainID] - if !ok { - return fmt.Errorf("chain selector not found for chainID: %d", cfg.ChainID) - } - - c.chainSelector = cs - c.id = "evm" + ":ChainSelector:" + strconv.FormatUint(cs, 10) + "@1.0.0" - - chainInfo, err := relayer.GetChainInfo(ctx) - if err != nil { - return fmt.Errorf("failed to fetch chain info for chainID %d from relayer: %w", cfg.ChainID, err) - } - - messageBuilder := monitoring.NewMessageBuilder(chainInfo, c.CapabilityInfo, cfg.NodeAddress) - - evmRelayer, err := relayer.EVM() - if err != nil { - return fmt.Errorf("failed to init evm relayer for chainID %d from relayer: %w", cfg.ChainID, err) - } - - consensusMetrics, err := consMetrics.NewConsensusMetrics(chainInfo) - if err != nil { - return fmt.Errorf("failed to create evm consensus metrics: %w", err) - } - c.requestPoller = poller.NewPoller(c.lggr, consensusMetrics, cfg.ObservationPollerWorkersCount, cfg.ObservationPollPeriod) - c.consensusHandler = chainconsensus.NewHandler(c.lggr, c.requestPoller, consensusMetrics, cfg.UnknownRequestsTTL) - - // capabilityDonID is the on-chain DON ID of the capability DON this plugin - // process serves, used to label emitted trigger events with the *sending* - // DON ID. The host (chainlink) resolves it authoritatively and injects it via - // dependencies.CapabilityDonID at Initialise time: - // - syncer boot path: always populated; - // - job-spec boot path: populated when unambiguous, otherwise 0 (e.g. a node - // that belongs to multiple DONs running this capability, or a core node - // that pre-dates CRE-4409). - // When it is 0 the trigger service falls back to the consumer workflow's DON - // ID (see trigger.NewLogTriggerService). We deliberately do NOT re-resolve it - // from the registry here: that lookup cannot disambiguate multi-DON nodes and - // would emit a guess instead of the safe workflow-DON fallback. See CRE-4409. - capabilityDonID := dependencies.CapabilityDonID + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + // The capability's own settings, bound directly: the chain, the client, the + // database and the keys are dependencies, and each binds its own. + cfg := config.Default + + root := &cobra.Command{ + Use: "evm", + Short: "The CRE EVM chain capability", + Long: `Runs the EVM capability, which reads an EVM chain for a workflow, fires its +log triggers, and writes the reports a DON agrees on. + +It reaches the chain itself: --evm.http-url is the RPC it dials, --chain.chain-id +says which chain that is, and --database.url plus --database.schema is where its +log poller and transaction manager keep their state - a schema of this +capability's own, so nothing it runs shares a table with the node's own relayers. +What it does not hold is keys or a peer - --keystore.proxy-address signs as this +node, --ocr.proxy-address carries the oracle's messages, 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 = "evm" + if err := flags.RegisterCommandFlags(root, &cfg, opts); err != nil { + return err + } + + bootstrapper := standalone.NewBootstrapper(root, + standalone.WithOtelViews(append(consMetrics.MetricViews(), monitoring.MetricViews()...))) + lggr := bootstrapper.Logger() + + // This capability's own database, in a schema of its own: the tables are + // chainlink-evm's, and the node's copies of them are not this capability's to + // share. + dbDep := chain.DBDependency(lggr.Named("Database"), embeddedMigrations, migrationsTable) + // The keys are the node's, borrowed a signature at a time: the transaction + // manager sends as the account the registry knows this node by. An embedded run + // has no node to borrow from and signs with keys derived from its index. + ksDep := keystore.Proxy(lggr.Named("Keystore"), embeddedKeystore(&cfg)) + // The client says where the chain is; the chain is built over it. An embedded run + // told about no chain gets one of its own, started in this process by the client + // dependency itself. + clientDep := creevm.Dependency(lggr.Named("EVMClient")) + // What a deployment would have put on that chain: the instances' accounts funded, + // and the forwarder they write reports through deployed and told who they are. + // Nothing at all when the run named a chain, which came with its own. + simDep := simulated.Dependency(lggr.Named("SimulatedChain"), clientDep.Simulated(), embeddedKeystore(&cfg)) + // Narrowed to this chain's account, the way a node's key states narrow the + // keystore a relayer is handed: the store behind the proxy holds a key per chain + // this node runs, and only one of them is this chain's transmitter. + chainDep := chain.Dependency(lggr.Named("Chain"), clientDep, dbDep, ksDep, &cfg.NodeAddress) + // 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.Run6(bootstrapper, func( + ctx context.Context, + scfg *standalone.StandaloneConfig, + evmChain legacyevm.Chain, + keys core.Keystore, + database *sql.DB, + factories *ocr.OCRFactories, + deps capability.Dependencies, + deployment *simulated.Deployment, + ) []services.Service { + lggr := scfg.Logger.Named("evm") + + // A simulated chain names its own forwarder: this process deployed it, and there + // was nothing to configure the capability with before it existed. + cfg := cfg + if deployment != nil { + cfg.CREForwarderAddress = deployment.Forwarder.Hex() + if cfg.ReceiverGasMinimum == 0 { + cfg.ReceiverGasMinimum = simulated.DefaultReceiverGasMinimum + } + } - var scheduler ts.TransmissionScheduler - if cfg.DeltaStage > 0 { - // The transmission scheduler needs this DON's membership/quorum. Pass the - // authoritative DON ID so a multi-DON node selects the correct DON; when it - // is 0, InitMyDON keeps its legacy "first matched DON" behavior. - myDON, err := ts.InitMyDON(ctx, dependencies.CapabilityRegistry, c.id, capabilityDonID, c.lggr, cfg.IsLocaL) + chainInfo, err := evmChain.GetChainInfo(ctx) if err != nil { - return fmt.Errorf("failed to init DON: %w", err) + lggr.Fatalw("Failed to read chain info", "error", err) } - c.DON = &myDON - c.lggr.Debugw("Initialised DON", "donID", c.DON.ID, "donName", c.DON.Name, "members", len(c.DON.Members), "F", c.DON.F) - scheduler, err = ts.InitialiseTransmissionScheduler(ctx, dependencies.CapabilityRegistry, cfg.DeltaStage, c.lggr, c.DON, cfg.IsLocaL) + // The chain's read and write surface, as a relayer would have handed it over. + evmService, err := chain.EVMService(lggr.Named("EVMService"), evmChain, database, keys, deps.CapabilityRegistry) if err != nil { - return fmt.Errorf("failed to initialize transmission scheduler: %w", err) + lggr.Fatalw("Failed to build the chain's service", "error", err) } - } else { - c.lggr.Infow("DeltaStage not configured, transmission scheduling disabled") - } - - c.EVM, err = actions.NewEVM(*cfg, evmRelayer, c.lggr, processor, messageBuilder, c.consensusHandler, c.chainSelector, c.limitsFactory, scheduler) - if err != nil { - return fmt.Errorf("failed to init evm relayer for chainID %d from relayer: %w", cfg.ChainID, err) - } - - // TODO: add org resolver - capabilityID := fmt.Sprintf("%s (%d)", c.id, cfg.ChainID) - c.triggerService, err = trigger.NewLogTriggerService(evmRelayer, trigger.NewLogTriggerStore(), c.lggr, capabilityID, capabilityDonID, processor, messageBuilder, - cfg.LogTriggerPollInterval, cfg.LogTriggerSendChannelBufferSize, cfg.LogTriggerLimitQueryLogSize, c.limitsFactory, - dependencies.OrgResolver, dependencies.TriggerEventStore) - if err != nil { - return fmt.Errorf("error when creating trigger: %w", err) - } - - c.heightProvider = height.NewProvider(c.lggr, cfg.ChainHeightPollPeriod, evmRelayer) - c.oracle, err = dependencies.OracleFactory.NewOracle(ctx, core.OracleArgs{ - LocalConfig: ocrtypes.LocalConfig{ - BlockchainTimeout: time.Second * 20, - ContractConfigTrackerPollInterval: time.Second * 10, - ContractConfigConfirmations: 1, - ContractTransmitterTransmitTimeout: time.Second * 10, - DatabaseTimeout: time.Second * 10, - ContractConfigLoadTimeout: time.Second * 10, - DefaultMaxDurationInitialization: time.Second * 10, - }, - ReportingPluginFactoryService: oracle.NewReportingPluginFactory(logger.Sugared(c.lggr), c.consensusHandler, c.heightProvider, consensusMetrics), - ContractTransmitter: oracle.NewContractTransmitter(c.lggr, c.consensusHandler), - }) - if err != nil { - return fmt.Errorf("error when creating oracle: %w", err) - } - - startServices := []interface{ Start(context.Context) error }{c.consensusHandler, c.requestPoller, c.oracle, c.heightProvider, c.triggerService} - for _, service := range startServices { - if err := service.Start(ctx); err != nil { - return err + capabilityImpl, err := New(lggr, cfg, Dependencies{ + EVMService: evmService, + ChainInfo: chainInfo, + DonID: deps.CapabilityDonID, + Registry: deps.OCRConfigRegistry, + CapabilityRegistry: deps.CapabilityRegistry, + Endpoints: factories.OCR2Endpoint, + Offchain: factories.Offchain, + Onchain: factories.Onchain, + TransmitAccount: factories.TransmitAccount, + Bootstrappers: factories.Bootstrappers, + // Trigger events outlive a restart, so they are kept where the chain state is, + // under this chain's ID: the schema holds every chain this node runs the + // capability on, the same way the chain tables do. + EventStore: eventstore.New(sqlx.NewDb(database, "pgx"), chainInfo.ChainID), + LimitsFactory: deps.LimitsFactory, + Metrics: scfg.MetricsRegisterer, + }) + if err != nil { + lggr.Fatalw("Failed to create the EVM capability", "error", err) } - } - c.lggr.Infof("Successfully initialised %s", CapabilityName) - return nil -} - -func (c *capabilityGRPCService) unmarshalConfig(configStr string) (*config.Config, error) { - var cfg config.Config - if err := json.Unmarshal([]byte(configStr), &cfg); err != nil { - return nil, fmt.Errorf("failed to parse EVM capability config: %w", err) - } - - if cfg.LogTriggerPollInterval < 0 { - return nil, fmt.Errorf("logTriggerPollInterval must be positive, got: %s", cfg.LogTriggerPollInterval) - } - - if !common.IsHexAddress(cfg.CREForwarderAddress) { - return nil, fmt.Errorf("invalid cre forward address, it does not have 20 characters: %s", cfg.CREForwarderAddress) - } - - if cfg.ReceiverGasMinimum == 0 { - return nil, fmt.Errorf("invalid ReceiverGasMinimum value. It must be greater than 0. Provided ReceiverGasMinimum %d", cfg.ReceiverGasMinimum) - } - - if cfg.ObservationPollerWorkersCount == 0 { - cfg.ObservationPollerWorkersCount = 10 - c.lggr.Infof("ObservationPollerWorkersCount is zero, setting to %d.", cfg.ObservationPollerWorkersCount) - } - - if cfg.ObservationPollPeriod == 0 { - cfg.ObservationPollPeriod = 2 * time.Second - c.lggr.Infof("ObservationPollPeriod is zero, setting to %s.", cfg.ObservationPollPeriod) - } - - if cfg.ChainHeightPollPeriod == 0 { - cfg.ChainHeightPollPeriod = time.Second - c.lggr.Infof("ChainHeightPollPeriod is zero, setting to %s.", cfg.ChainHeightPollPeriod) - } + // Run supervises the capability and makes it reachable: registered, served, + // and announced to the node's registry. + svcs, err := capability.Run(deps, *scfg, protos.NewClientServer(capabilityImpl)) + if err != nil { + lggr.Fatalw("Failed to host the EVM capability", "error", err) + } - if cfg.UnknownRequestsTTL == 0 { - cfg.UnknownRequestsTTL = 10 * time.Second - c.lggr.Infof("UnknownRequestsTTL is zero, setting to %s.", cfg.UnknownRequestsTTL) + // The chain goes first: it is what the capability answers with, and the + // bootstrapper starts services in the order given. + return append([]services.Service{evmChain}, svcs...) + }, chainDep, ksDep, dbDep, ocrDep, capDep, simDep) +} + +// embeddedKeystore is what an embedded instance signs this chain's transactions +// with: the key it was given, or - given none - the one its index derives. +// +// Configured keys are per instance and in instance order, because the instances +// of an embedded run are separate DON members: two of them sending from one +// account would be two transaction managers assigning the same nonces. +func embeddedKeystore(cfg *config.Config) func(instance int) (core.Keystore, error) { + return func(instance int) (core.Keystore, error) { + keys := cfg.PrivateKeys + if len(keys) == 0 { + return chain.DeterministicKeystore(instance) + } + if instance >= len(keys) { + return nil, fmt.Errorf("instance %d has no --evm.private-keys entry: %d were given, and every instance sends from an account of its own", instance, len(keys)) + } + return chain.KeystoreFromPrivateKey(keys[instance]) } - - // DeltaStage is optional - if not set, transmission scheduling will be disabled - return &cfg, nil -} - -func (c *capabilityGRPCService) Start(_ context.Context) error { - c.lggr.Infof("Start %s", CapabilityName) - return nil -} - -func (c *capabilityGRPCService) Close() error { - c.lggr.Infof("Closing %s", CapabilityName) - return errors.Join(c.EVM.Close(), c.requestPoller.Close(), c.consensusHandler.Close(), c.oracle.Close(context.Background()), c.triggerService.Close(), c.heightProvider.Close()) -} - -func (c *capabilityGRPCService) HealthReport() map[string]error { - return map[string]error{c.Name(): nil} -} - -func (c *capabilityGRPCService) Name() string { - return c.lggr.Name() -} - -func (c *capabilityGRPCService) ChainSelector() uint64 { - return c.chainSelector -} - -func (c *capabilityGRPCService) Description() string { - return "Contains EVM chain functionalities" -} - -func (c *capabilityGRPCService) Ready() error { - return nil -} - -func (c *capabilityGRPCService) RegisterToWorkflow(_ context.Context, _ capabilities.RegisterToWorkflowRequest) error { - return errors.New("not implemented") -} - -func (c *capabilityGRPCService) UnregisterFromWorkflow(_ context.Context, _ capabilities.UnregisterFromWorkflowRequest) error { - // TODO implement me - return errors.New("not implemented") -} - -func (c *capabilityGRPCService) RegisterLogTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *evmcappb.FilterLogTriggerRequest) (<-chan capabilities.TriggerAndId[*evmcappb.Log], caperrors.Error) { - return c.triggerService.RegisterLogTrigger(ctx, triggerID, metadata, input) -} - -func (c *capabilityGRPCService) UnregisterLogTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *evmcappb.FilterLogTriggerRequest) caperrors.Error { - return c.triggerService.UnregisterLogTrigger(ctx, triggerID, metadata, input) -} - -func (c *capabilityGRPCService) AckEvent(ctx context.Context, triggerID string, eventID string, method string) caperrors.Error { - return c.triggerService.AckEvent(ctx, triggerID, eventID) } diff --git a/chain_capabilities/evm/main_test.go b/chain_capabilities/evm/main_test.go index 7918a4b87..e7955905d 100644 --- a/chain_capabilities/evm/main_test.go +++ b/chain_capabilities/evm/main_test.go @@ -1,12 +1,9 @@ package main import ( - "context" - "encoding/json" "testing" "time" - "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -16,12 +13,11 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/settings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" - relayermock "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" evmmock "github.com/smartcontractkit/chainlink-common/pkg/types/mocks" "github.com/smartcontractkit/chainlink-evm/pkg/testutils" "github.com/smartcontractkit/capabilities/chain_capabilities/evm/config" + libsocr "github.com/smartcontractkit/capabilities/libs/ocr" ) func testLimitsFactory(t *testing.T) limits.Factory { @@ -31,136 +27,122 @@ func testLimitsFactory(t *testing.T) limits.Factory { return limits.Factory{Settings: g} } -func TestCapabilityGRPCService_Initialise(t *testing.T) { +// testConfig is a configuration that runs: the two required settings filled in, +// and the defaults everywhere else. +func testConfig(t *testing.T) config.Config { + t.Helper() + cfg := config.Default + cfg.CREForwarderAddress = testutils.NewAddress().String() + cfg.ReceiverGasMinimum = 1000 + cfg.LogTriggerPollInterval = 60 * time.Second + // Staggering needs a DON to place this node in, and there is no registry here. + cfg.IsLocal = true + cfg.DeltaStage = time.Second + return cfg +} + +// testDependencies are what the host would resolve, with the chain mocked and the +// oracle stubbed: what is under test is the capability being assembled and +// started, not libocr agreeing with itself. +func testDependencies(t *testing.T) Dependencies { + t.Helper() + evmSvc := evmmock.NewEVMService(t) + evmSvc.On("GetFiltersNames", mock.Anything).Maybe().Return([]string{}, nil) + + return Dependencies{ + EVMService: evmSvc, + ChainInfo: types.ChainInfo{FamilyName: "evm", ChainID: "1337"}, + EventStore: capabilities.NewMemEventStore(), + LimitsFactory: testLimitsFactory(t), + NewOracle: func(libsocr.OracleArgs) (Oracle, error) { return stubOracle{}, nil }, + } +} + +func TestNew(t *testing.T) { t.Parallel() - t.Run("happy-path", func(t *testing.T) { - evmSvc := evmmock.NewEVMService(t) - evmSvc.On("GetFiltersNames", mock.Anything).Maybe().Return([]string{}, nil) - relayer := relayermock.NewRelayer(t) - relayer.On("EVM").Return(evmSvc, nil) - relayer.On("GetChainInfo", mock.Anything).Return(types.ChainInfo{}, nil) - - relayerSet := relayermock.NewRelayerSet(t) - relayerSet.On("Get", mock.Anything, mock.Anything).Return(relayer, nil) - svc := &capabilityGRPCService{lggr: logger.Test(t), limitsFactory: testLimitsFactory(t)} - cfg := config.Config{ChainID: 1337, Network: "testnet", LogTriggerPollInterval: 60 * time.Second, CREForwarderAddress: testutils.NewAddress().String(), ReceiverGasMinimum: 1000, IsLocaL: true, DeltaStage: time.Second} - cfgJSON, _ := json.Marshal(cfg) - - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{OracleFactory: nullOracleFactory{}, RelayerSet: relayerSet, Config: string(cfgJSON), TriggerEventStore: capabilities.NewMemEventStore()}) - require.NoError(t, err) - require.NoError(t, svc.Close()) - }) - t.Run("happy-path-with-triggers-params", func(t *testing.T) { - evmSvc := evmmock.NewEVMService(t) - evmSvc.On("GetFiltersNames", mock.Anything).Maybe().Return([]string{}, nil) - relayer := relayermock.NewRelayer(t) - relayer.On("EVM").Return(evmSvc, nil) - relayer.On("GetChainInfo", mock.Anything).Return(types.ChainInfo{}, nil) - - relayerSet := relayermock.NewRelayerSet(t) - relayerSet.On("Get", mock.Anything, mock.Anything).Return(relayer, nil) - svc := &capabilityGRPCService{lggr: logger.Test(t), limitsFactory: testLimitsFactory(t)} - cfg := config.Config{ChainID: 1337, Network: "testnet", LogTriggerPollInterval: 60 * time.Second, LogTriggerSendChannelBufferSize: 100, LogTriggerLimitQueryLogSize: 10, CREForwarderAddress: common.Bytes2Hex(testutils.NewAddress().Bytes()), ReceiverGasMinimum: 1000, DeltaStage: time.Second, IsLocaL: true} - cfgJSON, _ := json.Marshal(cfg) - - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{OracleFactory: nullOracleFactory{}, RelayerSet: relayerSet, Config: string(cfgJSON), TriggerEventStore: capabilities.NewMemEventStore()}) + + // The capability is built here but not started: starting it joins a protocol and + // polls a chain, which is what the integration tests are for. + t.Run("builds", func(t *testing.T) { + _, err := New(logger.Test(t), testConfig(t), testDependencies(t)) require.NoError(t, err) - require.NoError(t, svc.Close()) }) - t.Run("bad-json", func(t *testing.T) { - svc := &capabilityGRPCService{lggr: logger.Test(t)} - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{OracleFactory: nullOracleFactory{}, Config: "x"}) - assert.ErrorContains(t, err, "failed to parse") - }) - t.Run("bad-trigger-params", func(t *testing.T) { - evmSvc := evmmock.NewEVMService(t) - evmSvc.On("GetFiltersNames", mock.Anything).Maybe().Return([]string{}, nil) - relayer := relayermock.NewRelayer(t) - relayer.On("EVM").Return(evmSvc, nil) - relayer.On("GetChainInfo", mock.Anything).Return(types.ChainInfo{}, nil) - - relayerSet := relayermock.NewRelayerSet(t) - relayerSet.On("Get", mock.Anything, mock.Anything).Return(relayer, nil) - svc := &capabilityGRPCService{lggr: logger.Test(t)} - - cfg := config.Config{ChainID: 1337, Network: "testnet", LogTriggerPollInterval: -1, CREForwarderAddress: common.Bytes2Hex(testutils.NewAddress().Bytes()), ReceiverGasMinimum: 1000, DeltaStage: time.Second, IsLocaL: true} - cfgJSON, _ := json.Marshal(cfg) - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{OracleFactory: nullOracleFactory{}, RelayerSet: relayerSet, Config: string(cfgJSON)}) - - assert.ErrorContains(t, err, "failed to unmarshal config: logTriggerPollInterval must be positive, got: -1ns") - - cfg = config.Config{ChainID: 1337, Network: "testnet", LogTriggerPollInterval: 60 * time.Second, CREForwarderAddress: common.Bytes2Hex(testutils.NewAddress().Bytes()), ReceiverGasMinimum: 1000, LogTriggerLimitQueryLogSize: uint64(1001), DeltaStage: time.Second, IsLocaL: true} - cfgJSON, _ = json.Marshal(cfg) - err = svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{OracleFactory: nullOracleFactory{}, RelayerSet: relayerSet, Config: string(cfgJSON)}) - assert.ErrorContains(t, err, "error when creating trigger: logTriggerLimitQueryLogSize (1001) must be less than logTriggerSendChannelBufferSize (1000)") - - cfg = config.Config{ChainID: 1337, Network: "testnet", LogTriggerPollInterval: 60 * time.Second, CREForwarderAddress: common.Bytes2Hex(testutils.NewAddress().Bytes()), ReceiverGasMinimum: 1000, - LogTriggerSendChannelBufferSize: 5, LogTriggerLimitQueryLogSize: uint64(10), DeltaStage: time.Second, IsLocaL: true} - cfgJSON, _ = json.Marshal(cfg) - err = svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{OracleFactory: nullOracleFactory{}, RelayerSet: relayerSet, Config: string(cfgJSON)}) - assert.ErrorContains(t, err, "error when creating trigger: logTriggerLimitQueryLogSize (10) must be less than logTriggerSendChannelBufferSize (5)") - }) - t.Run("relayerSet error", func(t *testing.T) { - relayerSet := relayermock.NewRelayerSet(t) - relayerSet.On("Get", mock.Anything, mock.Anything).Return(nil, assert.AnError) + t.Run("builds with the trigger's buffers configured", func(t *testing.T) { + cfg := testConfig(t) + cfg.LogTriggerSendChannelBufferSize = 100 + cfg.LogTriggerLimitQueryLogSize = 10 - cfgJSON, _ := json.Marshal(config.Config{ChainID: 1, Network: "net", LogTriggerPollInterval: 60 * time.Second, CREForwarderAddress: testutils.NewAddress().String(), ReceiverGasMinimum: 1000, DeltaStage: time.Second, IsLocaL: true}) - svc := &capabilityGRPCService{lggr: logger.Test(t)} + _, err := New(logger.Test(t), cfg, testDependencies(t)) + require.NoError(t, err) + }) - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{RelayerSet: relayerSet, Config: string(cfgJSON)}) + t.Run("builds without staggered transmission", func(t *testing.T) { + cfg := testConfig(t) + cfg.DeltaStage = 0 - assert.ErrorIs(t, err, assert.AnError) + _, err := New(logger.Test(t), cfg, testDependencies(t)) + require.NoError(t, err) }) - t.Run("happy-path-without-delta-stage", func(t *testing.T) { - evmSvc := evmmock.NewEVMService(t) - evmSvc.On("GetFiltersNames", mock.Anything).Maybe().Return([]string{}, nil) - relayer := relayermock.NewRelayer(t) - relayer.On("EVM").Return(evmSvc, nil) - relayer.On("GetChainInfo", mock.Anything).Return(types.ChainInfo{}, nil) - - relayerSet := relayermock.NewRelayerSet(t) - relayerSet.On("Get", mock.Anything, mock.Anything).Return(relayer, nil) - svc := &capabilityGRPCService{lggr: logger.Test(t), limitsFactory: testLimitsFactory(t)} - cfg := config.Config{ChainID: 1337, Network: "testnet", LogTriggerPollInterval: 60 * time.Second, CREForwarderAddress: testutils.NewAddress().String(), ReceiverGasMinimum: 1000, IsLocaL: true} - cfgJSON, _ := json.Marshal(cfg) - - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{OracleFactory: nullOracleFactory{}, RelayerSet: relayerSet, Config: string(cfgJSON), TriggerEventStore: capabilities.NewMemEventStore()}) + + t.Run("the capability ID names the chain", func(t *testing.T) { + c, err := New(logger.Test(t), testConfig(t), testDependencies(t)) require.NoError(t, err) - require.NoError(t, svc.Close()) + + // 1337 is the selector chain-selectors gives chain ID 1337, and a workflow asks + // for a chain by that selector rather than for EVM in general. + assert.NotZero(t, c.ChainSelector()) + assert.Contains(t, c.id, "ChainSelector:") }) - t.Run("Misconfiguration", func(t *testing.T) { - t.Run("No Keystone forwarder address provided", func(t *testing.T) { - cfgJSON, _ := json.Marshal(config.Config{ChainID: 1, Network: "net", ReceiverGasMinimum: 1000}) - svc := &capabilityGRPCService{lggr: logger.Test(t)} - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{Config: string(cfgJSON)}) + t.Run("a chain with no selector is refused", func(t *testing.T) { + deps := testDependencies(t) + deps.ChainInfo.ChainID = "424242424242" - assert.ErrorContains(t, err, "invalid cre forward address, it does not have 20 characters") - }) + _, err := New(logger.Test(t), testConfig(t), deps) + assert.ErrorContains(t, err, "no chain selector for chain ID 424242424242") + }) - t.Run("ReceiverGasConfig zero", func(t *testing.T) { - cfgJSON, _ := json.Marshal(config.Config{ChainID: 1, Network: "net", ReceiverGasMinimum: 0, CREForwarderAddress: testutils.NewAddress().String()}) - svc := &capabilityGRPCService{lggr: logger.Test(t)} + t.Run("a trigger buffer smaller than a query is refused", func(t *testing.T) { + cfg := testConfig(t) + cfg.LogTriggerSendChannelBufferSize = 5 + cfg.LogTriggerLimitQueryLogSize = 10 - err := svc.Initialise(t.Context(), core.StandardCapabilitiesDependencies{Config: string(cfgJSON)}) - assert.ErrorContains(t, err, "invalid ReceiverGasMinimum value. It must be greater than 0. Provided ReceiverGasMinimum 0") - }) + _, err := New(logger.Test(t), cfg, testDependencies(t)) + assert.ErrorContains(t, err, "logTriggerLimitQueryLogSize (10) must be less than logTriggerSendChannelBufferSize (5)") }) } -type nullOracleFactory struct{} +func TestConfigValidate(t *testing.T) { + t.Parallel() -func (nullOracleFactory) NewOracle(ctx context.Context, args core.OracleArgs) (core.Oracle, error) { - return nullOracle{}, nil -} + t.Run("a configuration that runs", func(t *testing.T) { + require.NoError(t, testConfig(t).Validate()) + }) -type nullOracle struct{} + t.Run("no forwarder address", func(t *testing.T) { + cfg := testConfig(t) + cfg.CREForwarderAddress = "" + assert.ErrorContains(t, cfg.Validate(), "is not an address") + }) -func (nullOracle) Start(ctx context.Context) error { - return nil -} + t.Run("no receiver gas floor", func(t *testing.T) { + cfg := testConfig(t) + cfg.ReceiverGasMinimum = 0 + assert.ErrorContains(t, cfg.Validate(), "--evm.receiver-gas-minimum must be greater than 0") + }) -func (nullOracle) Close(ctx context.Context) error { - return nil + t.Run("a negative trigger poll interval", func(t *testing.T) { + cfg := testConfig(t) + cfg.LogTriggerPollInterval = -1 + assert.ErrorContains(t, cfg.Validate(), "must not be negative") + }) } + +// stubOracle stands in for libocr, which needs a DON to agree with and a registry +// to read its configuration from - neither of which a unit test has. +type stubOracle struct{} + +func (stubOracle) Start() error { return nil } + +func (stubOracle) Close() error { return nil } diff --git a/chain_capabilities/evm/migrations/0001_evm_schema.sql b/chain_capabilities/evm/migrations/0001_evm_schema.sql new file mode 100644 index 000000000..1854e7f88 --- /dev/null +++ b/chain_capabilities/evm/migrations/0001_evm_schema.sql @@ -0,0 +1,503 @@ +-- +goose Up +-- The tables chainlink-evm keeps its state in: the log poller's blocks, filters and logs, the head +-- tracker's heads, and the transaction manager's transactions, attempts and receipts. +-- +-- They are written against the evm schema, because that is the schema chainlink-evm's queries name. +-- Neither this nor those queries reach the schema of that name: the connection this runs on rewrites +-- the qualifier into the schema this capability was configured with (see chain/db.go), so the tables +-- land wherever that is - one schema per capability, and one per instance of an embedded run, none of +-- them the node's. +-- +-- The DDL is the shape a node's migrations arrive at, collapsed into one: this capability has no +-- history to replay. It was produced by migrating a node's database and dumping the result, with the +-- tables nothing here reads left out (key states, which this keystore does not use, and automation's +-- upkeep states). Keeping it identical to the dump is what makes it comparable to a node's schema +-- when chainlink-evm changes one. + +-- tx_attempts_state (type) +CREATE TYPE evm.tx_attempts_state AS ENUM ( + 'in_progress', + 'insufficient_eth', + 'broadcast' +); + +-- txes_state (type) +CREATE TYPE evm.txes_state AS ENUM ( + 'unstarted', + 'in_progress', + 'fatal_error', + 'unconfirmed', + 'confirmed_missing_receipt', + 'confirmed', + 'finalized' +); + +-- f_log_poller_filter_hash(text, numeric, bytea, bytea, bytea, bytea, bytea) (function) +CREATE FUNCTION evm.f_log_poller_filter_hash(name text, evm_chain_id numeric, address bytea, event bytea, topic2 bytea, topic3 bytea, topic4 bytea) RETURNS bigint + LANGUAGE sql IMMUTABLE COST 25 PARALLEL SAFE + AS $_$SELECT hashtextextended(textin(record_out(($1,$2,$3,$4,$5,$6,$7))), 0)$_$; + + +-- receipts (table) +CREATE TABLE evm.receipts ( + id bigint NOT NULL, + tx_hash bytea NOT NULL, + block_hash bytea NOT NULL, + block_number bigint NOT NULL, + transaction_index bigint NOT NULL, + receipt jsonb NOT NULL, + created_at timestamp with time zone NOT NULL, + CONSTRAINT chk_hash_length CHECK (((octet_length(tx_hash) = 32) AND (octet_length(block_hash) = 32))) +); + +-- eth_receipts_id_seq (sequence) +CREATE SEQUENCE evm.eth_receipts_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- eth_receipts_id_seq (sequence owned by) +ALTER SEQUENCE evm.eth_receipts_id_seq OWNED BY evm.receipts.id; + +-- tx_attempts (table) +CREATE TABLE evm.tx_attempts ( + id bigint NOT NULL, + eth_tx_id bigint NOT NULL, + gas_price numeric(78,0), + signed_raw_tx bytea NOT NULL, + hash bytea NOT NULL, + broadcast_before_block_num bigint, + state evm.tx_attempts_state NOT NULL, + created_at timestamp with time zone NOT NULL, + chain_specific_gas_limit bigint NOT NULL, + tx_type smallint DEFAULT 0 NOT NULL, + gas_tip_cap numeric(78,0), + gas_fee_cap numeric(78,0), + is_purge_attempt boolean DEFAULT false NOT NULL, + CONSTRAINT chk_cannot_broadcast_before_block_zero CHECK (((broadcast_before_block_num IS NULL) OR (broadcast_before_block_num > 0))), + CONSTRAINT chk_chain_specific_gas_limit_not_zero CHECK ((chain_specific_gas_limit > 0)), + CONSTRAINT chk_eth_tx_attempts_fsm CHECK ((((state = ANY (ARRAY['in_progress'::evm.tx_attempts_state, 'insufficient_eth'::evm.tx_attempts_state])) AND (broadcast_before_block_num IS NULL)) OR (state = 'broadcast'::evm.tx_attempts_state))), + CONSTRAINT chk_hash_length CHECK ((octet_length(hash) = 32)), + CONSTRAINT chk_legacy_or_dynamic CHECK ((((tx_type = 0) AND (gas_price IS NOT NULL) AND (gas_tip_cap IS NULL) AND (gas_fee_cap IS NULL)) OR ((tx_type = 2) AND (gas_price IS NULL) AND (gas_tip_cap IS NOT NULL) AND (gas_fee_cap IS NOT NULL)))), + CONSTRAINT chk_sanity_fee_cap_tip_cap CHECK (((gas_tip_cap IS NULL) OR (gas_fee_cap IS NULL) OR (gas_tip_cap <= gas_fee_cap))), + CONSTRAINT chk_signed_raw_tx_present CHECK ((octet_length(signed_raw_tx) > 0)), + CONSTRAINT chk_tx_type_is_byte CHECK (((tx_type >= 0) AND (tx_type <= 255))) +); + +-- eth_tx_attempts_id_seq (sequence) +CREATE SEQUENCE evm.eth_tx_attempts_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- eth_tx_attempts_id_seq (sequence owned by) +ALTER SEQUENCE evm.eth_tx_attempts_id_seq OWNED BY evm.tx_attempts.id; + +-- txes (table) +CREATE TABLE evm.txes ( + id bigint NOT NULL, + nonce bigint, + from_address bytea NOT NULL, + to_address bytea NOT NULL, + encoded_payload bytea NOT NULL, + value numeric(78,0) NOT NULL, + gas_limit bigint NOT NULL, + error text, + broadcast_at timestamp with time zone, + created_at timestamp with time zone NOT NULL, + meta jsonb, + subject uuid, + pipeline_task_run_id uuid, + min_confirmations integer, + evm_chain_id numeric(78,0) NOT NULL, + transmit_checker jsonb, + initial_broadcast_at timestamp with time zone, + idempotency_key character varying(2000), + signal_callback boolean DEFAULT false, + callback_completed boolean DEFAULT false, + state evm.txes_state DEFAULT 'unstarted'::evm.txes_state NOT NULL, + CONSTRAINT chk_broadcast_at_is_sane CHECK ((broadcast_at > '2019-01-01 00:00:00+00'::timestamp with time zone)), + CONSTRAINT chk_error_cannot_be_empty CHECK (((error IS NULL) OR (length(error) > 0))), + CONSTRAINT chk_from_address_length CHECK ((octet_length(from_address) = 20)), + CONSTRAINT chk_to_address_length CHECK ((octet_length(to_address) = 20)) +); + +-- eth_txes_id_seq (sequence) +CREATE SEQUENCE evm.eth_txes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- eth_txes_id_seq (sequence owned by) +ALTER SEQUENCE evm.eth_txes_id_seq OWNED BY evm.txes.id; + +-- forwarders (table) +CREATE TABLE evm.forwarders ( + id bigint NOT NULL, + address bytea NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + evm_chain_id numeric(78,0) NOT NULL, + CONSTRAINT chk_address_length CHECK ((octet_length(address) = 20)) +); + +-- evm_forwarders_id_seq (sequence) +CREATE SEQUENCE evm.evm_forwarders_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- evm_forwarders_id_seq (sequence owned by) +ALTER SEQUENCE evm.evm_forwarders_id_seq OWNED BY evm.forwarders.id; + +-- log_poller_filters (table) +CREATE TABLE evm.log_poller_filters ( + id bigint NOT NULL, + name text NOT NULL, + address bytea NOT NULL, + event bytea NOT NULL, + evm_chain_id numeric(78,0), + created_at timestamp with time zone NOT NULL, + retention bigint DEFAULT 0, + topic2 bytea, + topic3 bytea, + topic4 bytea, + max_logs_kept bigint DEFAULT 0 NOT NULL, + logs_per_block bigint DEFAULT 0 NOT NULL, + is_legacy_name boolean DEFAULT false, + CONSTRAINT evm_log_poller_filters_address_check CHECK ((octet_length(address) = 20)), + CONSTRAINT evm_log_poller_filters_event_check CHECK ((octet_length(event) = 32)), + CONSTRAINT evm_log_poller_filters_name_check CHECK ((length(name) > 0)), + CONSTRAINT log_poller_filters_topic2_check CHECK ((octet_length(topic2) = 32)), + CONSTRAINT log_poller_filters_topic3_check CHECK ((octet_length(topic3) = 32)), + CONSTRAINT log_poller_filters_topic4_check CHECK ((octet_length(topic4) = 32)) +); + +-- evm_log_poller_filters_id_seq (sequence) +CREATE SEQUENCE evm.evm_log_poller_filters_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- evm_log_poller_filters_id_seq (sequence owned by) +ALTER SEQUENCE evm.evm_log_poller_filters_id_seq OWNED BY evm.log_poller_filters.id; + +-- heads (table) +CREATE TABLE evm.heads ( + deprecated_id bigint NOT NULL, + hash bytea NOT NULL, + number bigint NOT NULL, + parent_hash bytea NOT NULL, + created_at timestamp with time zone NOT NULL, + "timestamp" timestamp with time zone NOT NULL, + l1_block_number bigint, + evm_chain_id numeric(78,0) NOT NULL, + base_fee_per_gas numeric(78,0), + CONSTRAINT chk_hash_size CHECK ((octet_length(hash) = 32)), + CONSTRAINT chk_parent_hash_size CHECK ((octet_length(parent_hash) = 32)) +); + +-- heads_id_seq (sequence) +CREATE SEQUENCE evm.heads_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- heads_id_seq (sequence owned by) +ALTER SEQUENCE evm.heads_id_seq OWNED BY evm.heads.deprecated_id; + +-- log_poller_blocks (table) +CREATE TABLE evm.log_poller_blocks ( + evm_chain_id numeric(78,0) NOT NULL, + block_hash bytea NOT NULL, + block_number bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + block_timestamp timestamp with time zone NOT NULL, + finalized_block_number bigint DEFAULT 0 NOT NULL, + id bigint NOT NULL, + safe_block_number bigint DEFAULT 0 NOT NULL, + CONSTRAINT log_poller_blocks_block_number_check CHECK ((block_number > 0)), + CONSTRAINT log_poller_blocks_finalized_block_number_check CHECK ((finalized_block_number >= 0)), + CONSTRAINT log_poller_blocks_safe_block_number_check CHECK ((safe_block_number >= 0)) +); + +-- log_poller_blocks_id_seq (sequence) +ALTER TABLE evm.log_poller_blocks ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME evm.log_poller_blocks_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + +-- logs (table) +CREATE TABLE evm.logs ( + evm_chain_id numeric(78,0) NOT NULL, + log_index bigint NOT NULL, + block_hash bytea NOT NULL, + block_number bigint NOT NULL, + address bytea NOT NULL, + event_sig bytea NOT NULL, + topics bytea[] NOT NULL, + tx_hash bytea NOT NULL, + data bytea NOT NULL, + created_at timestamp with time zone NOT NULL, + block_timestamp timestamp with time zone NOT NULL, + id bigint NOT NULL, + CONSTRAINT logs_block_number_check CHECK ((block_number > 0)) +); + +-- logs_id_seq (sequence) +ALTER TABLE evm.logs ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME evm.logs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + +-- forwarders id (default) +ALTER TABLE ONLY evm.forwarders ALTER COLUMN id SET DEFAULT nextval('evm.evm_forwarders_id_seq'::regclass); + +-- heads deprecated_id (default) +ALTER TABLE ONLY evm.heads ALTER COLUMN deprecated_id SET DEFAULT nextval('evm.heads_id_seq'::regclass); + +-- log_poller_filters id (default) +ALTER TABLE ONLY evm.log_poller_filters ALTER COLUMN id SET DEFAULT nextval('evm.evm_log_poller_filters_id_seq'::regclass); + +-- receipts id (default) +ALTER TABLE ONLY evm.receipts ALTER COLUMN id SET DEFAULT nextval('evm.eth_receipts_id_seq'::regclass); + +-- tx_attempts id (default) +ALTER TABLE ONLY evm.tx_attempts ALTER COLUMN id SET DEFAULT nextval('evm.eth_tx_attempts_id_seq'::regclass); + +-- txes id (default) +ALTER TABLE ONLY evm.txes ALTER COLUMN id SET DEFAULT nextval('evm.eth_txes_id_seq'::regclass); + +-- log_poller_blocks block_hash_uniq (constraint) +ALTER TABLE ONLY evm.log_poller_blocks + ADD CONSTRAINT block_hash_uniq UNIQUE (evm_chain_id, block_hash); + +-- txes chk_eth_txes_fsm (check constraint) +ALTER TABLE evm.txes + ADD CONSTRAINT chk_eth_txes_fsm CHECK ((((state = 'unstarted'::evm.txes_state) AND (nonce IS NULL) AND (error IS NULL) AND (broadcast_at IS NULL) AND (initial_broadcast_at IS NULL)) OR ((state = 'in_progress'::evm.txes_state) AND (nonce IS NOT NULL) AND (error IS NULL) AND (broadcast_at IS NULL) AND (initial_broadcast_at IS NULL)) OR ((state = 'fatal_error'::evm.txes_state) AND (error IS NOT NULL)) OR ((state = 'unconfirmed'::evm.txes_state) AND (nonce IS NOT NULL) AND (error IS NULL) AND (broadcast_at IS NOT NULL) AND (initial_broadcast_at IS NOT NULL)) OR ((state = 'confirmed'::evm.txes_state) AND (nonce IS NOT NULL) AND (error IS NULL) AND (broadcast_at IS NOT NULL) AND (initial_broadcast_at IS NOT NULL)) OR ((state = 'confirmed_missing_receipt'::evm.txes_state) AND (nonce IS NOT NULL) AND (error IS NULL) AND (broadcast_at IS NOT NULL) AND (initial_broadcast_at IS NOT NULL)) OR ((state = 'finalized'::evm.txes_state) AND (nonce IS NOT NULL) AND (error IS NULL) AND (broadcast_at IS NOT NULL) AND (initial_broadcast_at IS NOT NULL)))) NOT VALID; + +-- receipts eth_receipts_pkey (constraint) +ALTER TABLE ONLY evm.receipts + ADD CONSTRAINT eth_receipts_pkey PRIMARY KEY (id); + +-- tx_attempts eth_tx_attempts_pkey (constraint) +ALTER TABLE ONLY evm.tx_attempts + ADD CONSTRAINT eth_tx_attempts_pkey PRIMARY KEY (id); + +-- txes eth_txes_idempotency_key_key (constraint) +ALTER TABLE ONLY evm.txes + ADD CONSTRAINT eth_txes_idempotency_key_key UNIQUE (idempotency_key); + +-- txes eth_txes_pkey (constraint) +ALTER TABLE ONLY evm.txes + ADD CONSTRAINT eth_txes_pkey PRIMARY KEY (id); + +-- forwarders evm_forwarders_address_key (constraint) +ALTER TABLE ONLY evm.forwarders + ADD CONSTRAINT evm_forwarders_address_key UNIQUE (address); + +-- forwarders evm_forwarders_pkey (constraint) +ALTER TABLE ONLY evm.forwarders + ADD CONSTRAINT evm_forwarders_pkey PRIMARY KEY (id); + +-- log_poller_filters evm_log_poller_filters_pkey (constraint) +ALTER TABLE ONLY evm.log_poller_filters + ADD CONSTRAINT evm_log_poller_filters_pkey PRIMARY KEY (id); + +-- log_poller_blocks log_poller_blocks_pkey (constraint) +ALTER TABLE ONLY evm.log_poller_blocks + ADD CONSTRAINT log_poller_blocks_pkey PRIMARY KEY (id); + +-- logs logs_pkey (constraint) +ALTER TABLE ONLY evm.logs + ADD CONSTRAINT logs_pkey PRIMARY KEY (id); + +-- evm_logs_by_timestamp (index) +CREATE INDEX evm_logs_by_timestamp ON evm.logs USING btree (evm_chain_id, address, event_sig, block_timestamp, block_number); + +-- evm_logs_idx (index) +CREATE INDEX evm_logs_idx ON evm.logs USING btree (evm_chain_id, block_number, address, event_sig); + +-- evm_logs_idx_data_word_five (index) +CREATE INDEX evm_logs_idx_data_word_five ON evm.logs USING btree (address, event_sig, evm_chain_id, "substring"(data, 129, 32)); + +-- evm_logs_idx_data_word_four (index) +CREATE INDEX evm_logs_idx_data_word_four ON evm.logs USING btree (SUBSTRING(data FROM 97 FOR 32)); + +-- evm_logs_idx_data_word_one (index) +CREATE INDEX evm_logs_idx_data_word_one ON evm.logs USING btree (SUBSTRING(data FROM 1 FOR 32)); + +-- evm_logs_idx_data_word_three (index) +CREATE INDEX evm_logs_idx_data_word_three ON evm.logs USING btree (SUBSTRING(data FROM 65 FOR 32)); + +-- evm_logs_idx_data_word_two (index) +CREATE INDEX evm_logs_idx_data_word_two ON evm.logs USING btree (SUBSTRING(data FROM 33 FOR 32)); + +-- evm_logs_idx_topic_four (index) +CREATE INDEX evm_logs_idx_topic_four ON evm.logs USING btree ((topics[4])); + +-- evm_logs_idx_topic_three (index) +CREATE INDEX evm_logs_idx_topic_three ON evm.logs USING btree ((topics[3])); + +-- evm_logs_idx_topic_two (index) +CREATE INDEX evm_logs_idx_topic_two ON evm.logs USING btree ((topics[2])); + +-- evm_logs_idx_tx_hash (index) +CREATE INDEX evm_logs_idx_tx_hash ON evm.logs USING btree (tx_hash); + +-- idx_eth_receipts_block_number (index) +CREATE INDEX idx_eth_receipts_block_number ON evm.receipts USING btree (block_number); + +-- idx_eth_receipts_created_at (index) +CREATE INDEX idx_eth_receipts_created_at ON evm.receipts USING brin (created_at); + +-- idx_eth_receipts_unique (index) +CREATE UNIQUE INDEX idx_eth_receipts_unique ON evm.receipts USING btree (tx_hash, block_hash); + +-- idx_eth_tx_attempts_broadcast_before_block_num (index) +CREATE INDEX idx_eth_tx_attempts_broadcast_before_block_num ON evm.tx_attempts USING btree (broadcast_before_block_num); + +-- idx_eth_tx_attempts_created_at (index) +CREATE INDEX idx_eth_tx_attempts_created_at ON evm.tx_attempts USING brin (created_at); + +-- idx_eth_tx_attempts_hash (index) +CREATE UNIQUE INDEX idx_eth_tx_attempts_hash ON evm.tx_attempts USING btree (hash); + +-- idx_eth_tx_attempts_unbroadcast (index) +CREATE INDEX idx_eth_tx_attempts_unbroadcast ON evm.tx_attempts USING btree (state) WHERE (state <> 'broadcast'::evm.tx_attempts_state); + +-- idx_eth_tx_attempts_unique_gas_prices (index) +CREATE UNIQUE INDEX idx_eth_tx_attempts_unique_gas_prices ON evm.tx_attempts USING btree (eth_tx_id, gas_price); + +-- idx_eth_txes_broadcast_at (index) +CREATE INDEX idx_eth_txes_broadcast_at ON evm.txes USING brin (broadcast_at); + +-- idx_eth_txes_created_at (index) +CREATE INDEX idx_eth_txes_created_at ON evm.txes USING brin (created_at); + +-- idx_eth_txes_from_address (index) +CREATE INDEX idx_eth_txes_from_address ON evm.txes USING btree (from_address); + +-- idx_eth_txes_initial_broadcast_at (index) +CREATE INDEX idx_eth_txes_initial_broadcast_at ON evm.txes USING brin (initial_broadcast_at); + +-- idx_eth_txes_min_unconfirmed_nonce_for_key_evm_chain_id (index) +CREATE INDEX idx_eth_txes_min_unconfirmed_nonce_for_key_evm_chain_id ON evm.txes USING btree (evm_chain_id, from_address, nonce) WHERE (state = 'unconfirmed'::evm.txes_state); + +-- idx_eth_txes_nonce_from_address_per_evm_chain_id (index) +CREATE UNIQUE INDEX idx_eth_txes_nonce_from_address_per_evm_chain_id ON evm.txes USING btree (evm_chain_id, from_address, nonce); + +-- idx_eth_txes_pipeline_run_task_id (index) +CREATE UNIQUE INDEX idx_eth_txes_pipeline_run_task_id ON evm.txes USING btree (pipeline_task_run_id) WHERE (pipeline_task_run_id IS NOT NULL); + +-- idx_eth_txes_state_from_address_evm_chain_id (index) +CREATE INDEX idx_eth_txes_state_from_address_evm_chain_id ON evm.txes USING btree (evm_chain_id, from_address, state) WHERE ((state <> 'confirmed'::evm.txes_state) AND (state <> 'finalized'::evm.txes_state)); + +-- idx_eth_txes_unstarted_subject_id_evm_chain_id (index) +CREATE INDEX idx_eth_txes_unstarted_subject_id_evm_chain_id ON evm.txes USING btree (evm_chain_id, subject, id) WHERE ((subject IS NOT NULL) AND (state = 'unstarted'::evm.txes_state)); + +-- idx_evm_logs_ccip_exec_state_change_read (index) +CREATE INDEX idx_evm_logs_ccip_exec_state_change_read ON evm.logs USING btree (address, evm_chain_id, (topics[2]), (topics[3]), block_number, log_index, tx_hash) WHERE (event_sig = '\x05665fe9ad095383d018353f4cbcba77e84db27dd215081bbf7cdf9ae6fbe48b'::bytea); + +-- idx_evm_logs_ccip_message_sent_read_latest (index) +CREATE INDEX idx_evm_logs_ccip_message_sent_read_latest ON evm.logs USING btree (address, evm_chain_id, (topics[2]), block_number DESC) INCLUDE (topics, event_sig, block_number, log_index, tx_hash) WHERE (event_sig = '\x192442a2b2adb6a7948f097023cb6b57d29d3a7a5dd33e6666d33c39cc456f32'::bytea); + +-- idx_evm_logs_ccip_message_sent_read_seq (index) +CREATE INDEX idx_evm_logs_ccip_message_sent_read_seq ON evm.logs USING btree (address, evm_chain_id, (topics[2]), (topics[3]), block_number) INCLUDE (log_index, block_hash, event_sig, topics, tx_hash, created_at, block_timestamp) WHERE (event_sig = '\x192442a2b2adb6a7948f097023cb6b57d29d3a7a5dd33e6666d33c39cc456f32'::bytea); + +-- idx_forwarders_created_at (index) +CREATE INDEX idx_forwarders_created_at ON evm.forwarders USING brin (created_at); + +-- idx_forwarders_evm_address (index) +CREATE INDEX idx_forwarders_evm_address ON evm.forwarders USING btree (address); + +-- idx_forwarders_evm_chain_id (index) +CREATE INDEX idx_forwarders_evm_chain_id ON evm.forwarders USING btree (evm_chain_id); + +-- idx_forwarders_updated_at (index) +CREATE INDEX idx_forwarders_updated_at ON evm.forwarders USING brin (updated_at); + +-- idx_heads_evm_chain_id_hash (index) +CREATE UNIQUE INDEX idx_heads_evm_chain_id_hash ON evm.heads USING btree (evm_chain_id, hash); + +-- idx_heads_evm_chain_id_number (index) +CREATE INDEX idx_heads_evm_chain_id_number ON evm.heads USING btree (evm_chain_id, number); + +-- idx_log_poller_blocks_chain_block (index) +CREATE UNIQUE INDEX idx_log_poller_blocks_chain_block ON evm.log_poller_blocks USING btree (evm_chain_id, block_number DESC); + +-- idx_logs_chain_address_event_block_logindex (index) +CREATE INDEX idx_logs_chain_address_event_block_logindex ON evm.logs USING btree (evm_chain_id, address, event_sig, block_number); + +-- idx_logs_chain_block_logindex (index) +CREATE UNIQUE INDEX idx_logs_chain_block_logindex ON evm.logs USING btree (evm_chain_id, block_number, log_index); + +-- idx_only_one_in_progress_tx_per_account_id_per_evm_chain_id (index) +CREATE UNIQUE INDEX idx_only_one_in_progress_tx_per_account_id_per_evm_chain_id ON evm.txes USING btree (evm_chain_id, from_address) WHERE (state = 'in_progress'::evm.txes_state); + +-- idx_only_one_unbroadcast_attempt_per_eth_tx (index) +CREATE UNIQUE INDEX idx_only_one_unbroadcast_attempt_per_eth_tx ON evm.tx_attempts USING btree (eth_tx_id) WHERE (state <> 'broadcast'::evm.tx_attempts_state); + +-- idx_receipts_tx_hash (index) +CREATE INDEX idx_receipts_tx_hash ON evm.receipts USING btree (tx_hash); + +-- idx_receipts_tx_hash_id (index) +CREATE INDEX idx_receipts_tx_hash_id ON evm.receipts USING btree (tx_hash, id); + +-- idx_tx_attempts_eth_tx_id_hash (index) +CREATE INDEX idx_tx_attempts_eth_tx_id_hash ON evm.tx_attempts USING btree (eth_tx_id, hash); + +-- idx_tx_attempts_eth_tx_id_state_hash (index) +CREATE INDEX idx_tx_attempts_eth_tx_id_state_hash ON evm.tx_attempts USING btree (eth_tx_id, state, hash); + +-- idx_txes_evm_chain_id_state_nonce (index) +CREATE INDEX idx_txes_evm_chain_id_state_nonce ON evm.txes USING btree (evm_chain_id, state, nonce); + +-- log_poller_filters_hash_key (index) +CREATE UNIQUE INDEX log_poller_filters_hash_key ON evm.log_poller_filters USING btree (evm.f_log_poller_filter_hash(name, evm_chain_id, address, event, topic2, topic3, topic4)); + +-- receipts eth_receipts_tx_hash_fkey (fk constraint) +ALTER TABLE ONLY evm.receipts + ADD CONSTRAINT eth_receipts_tx_hash_fkey FOREIGN KEY (tx_hash) REFERENCES evm.tx_attempts(hash) ON DELETE CASCADE; + +-- tx_attempts eth_tx_attempts_eth_tx_id_fkey (fk constraint) +ALTER TABLE ONLY evm.tx_attempts + ADD CONSTRAINT eth_tx_attempts_eth_tx_id_fkey FOREIGN KEY (eth_tx_id) REFERENCES evm.txes(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +-- +goose Down +DROP TABLE IF EXISTS evm.logs; +DROP TABLE IF EXISTS evm.log_poller_blocks; +DROP TABLE IF EXISTS evm.log_poller_filters; +DROP TABLE IF EXISTS evm.heads; +DROP TABLE IF EXISTS evm.receipts; +DROP TABLE IF EXISTS evm.tx_attempts; +DROP TABLE IF EXISTS evm.txes; +DROP TABLE IF EXISTS evm.forwarders; +DROP FUNCTION IF EXISTS evm.f_log_poller_filter_hash(text, numeric, bytea, bytea, bytea, bytea, bytea); +DROP TYPE IF EXISTS evm.tx_attempts_state; +DROP TYPE IF EXISTS evm.txes_state; diff --git a/chain_capabilities/evm/migrations/0002_trigger_pending_events.sql b/chain_capabilities/evm/migrations/0002_trigger_pending_events.sql new file mode 100644 index 000000000..bd9d41ade --- /dev/null +++ b/chain_capabilities/evm/migrations/0002_trigger_pending_events.sql @@ -0,0 +1,26 @@ +-- +goose Up +-- Trigger events that have fired but not been acknowledged, so a restart resends what was in flight +-- rather than dropping it. The shape is libs/standalone/eventstore's, which is what reads and writes +-- this. +-- +-- Unqualified, so it lands in the schema this capability was configured with, beside its chain state: +-- these events are this instance's, and an embedded run's instances must not answer each other's. +-- +-- scope is which chain the events belong to. Every other table here carries an evm_chain_id, which is +-- what lets one schema hold every chain a node runs this capability on; this one is not chainlink-evm's, +-- so it says the same thing in its own words, and the process on chain A neither lists nor deletes what +-- is owed on chain B. +CREATE TABLE trigger_pending_events ( + scope TEXT NOT NULL DEFAULT '', + trigger_id TEXT NOT NULL, + event_id TEXT NOT NULL, + payload BYTEA NOT NULL, + first_at TIMESTAMPTZ NOT NULL, + last_sent_at TIMESTAMPTZ NULL, + attempts INTEGER NOT NULL DEFAULT 0, + org_id TEXT NOT NULL DEFAULT '', + PRIMARY KEY (scope, trigger_id, event_id) +); + +-- +goose Down +DROP TABLE IF EXISTS trigger_pending_events; diff --git a/chain_capabilities/evm/monitoring/messages.go b/chain_capabilities/evm/monitoring/messages.go index 27091d72e..e5a607b0d 100644 --- a/chain_capabilities/evm/monitoring/messages.go +++ b/chain_capabilities/evm/monitoring/messages.go @@ -5,12 +5,14 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" - evmcap "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + evmcap "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/internal/contracts" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" diff --git a/chain_capabilities/evm/project.json b/chain_capabilities/evm/project.json index 5edf6f815..ba6a239af 100644 --- a/chain_capabilities/evm/project.json +++ b/chain_capabilities/evm/project.json @@ -1,7 +1,7 @@ { "name": "evm", "projectType": "capability", - "implicitDependencies": ["loopserver"], + "implicitDependencies": ["chainconsensus"], "tags": [], "targets": { "lint": { diff --git a/chain_capabilities/evm/protos/addchain/main.go b/chain_capabilities/evm/protos/addchain/main.go new file mode 100644 index 000000000..56ba29159 --- /dev/null +++ b/chain_capabilities/evm/protos/addchain/main.go @@ -0,0 +1,161 @@ +// Command addchain adds an EVM chain to this capability's client.proto. +// +// The chain selectors a workflow may name are a label on the capability's +// methods, defaulted in client.proto - so a chain the CRE supports is a chain +// listed there, and adding one by hand means finding a sorted array of a few +// hundred entries and getting the formatting right. This does that edit. +// +// It is chainlink-protos' cre/go/tools/add-evm-chain, brought here with the proto +// it edits: this capability owns its copy of client.proto (see the package +// beside this one), so the tool that maintains the list has to be the one that +// knows where this copy is. +// +// Usage, from anywhere in this module: +// +// go run ./protos/addchain -selector 16015286601757825753 +// go generate ./protos/gen +// +// The second command is what makes the edit mean anything: the proto is what the +// generated Go is generated from, and nothing reads the proto at runtime. +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/generator" +) + +// protoFile is the proto the chain list lives in, under this module's protos +// directory - the same place the generator reads them from. +const protoFile = "client.proto" + +func main() { + selector := flag.Uint64("selector", 0, "chain selector value (required)") + path := flag.String("proto", "", "proto file to edit; defaults to this module's "+filepath.Join(generator.Protos, protoFile)) + flag.Parse() + + if *selector == 0 { + fatal("selector is required") + } + + protoPath := *path + if protoPath == "" { + resolved, err := defaultProtoPath() + if err != nil { + fatal("%v", err) + } + protoPath = resolved + } + + // Look up chain ID from selector + chainId, err := chain_selectors.ChainIdFromSelector(*selector) + if err != nil { + fatal("selector %d not found: %v", *selector, err) + } + + // Get chain name from chain ID + chainName, err := chain_selectors.NameFromChainId(chainId) + if err != nil { + fatal("failed to get chain name for chain ID %d: %v", chainId, err) + } + + // Read proto file + content, err := os.ReadFile(protoPath) + if err != nil { + fatal("failed to read %s: %v", protoPath, err) + } + + // Check if already exists + if strings.Contains(string(content), fmt.Sprintf(`key: "%s"`, chainName)) { + fmt.Printf("chain %s already exists\n", chainName) + return + } + + // Parse, add, sort, rebuild + newContent, err := addChain(string(content), chainName, *selector) + if err != nil { + fatal("failed to add chain: %v", err) + } + + if err := os.WriteFile(protoPath, []byte(newContent), 0600); err != nil { + fatal("failed to write file: %v", err) + } + + fmt.Printf("added %s (selector: %d) to %s\n", chainName, *selector, protoPath) + fmt.Println("run \"go generate ./protos/gen\" to regenerate the Go this proto describes") +} + +// defaultProtoPath finds the proto relative to the module rather than to the +// working directory, so this can be run from anywhere under it - the same rule +// the generator beside it follows. +func defaultProtoPath() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to read the working directory: %w", err) + } + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return filepath.Join(dir, generator.Protos, protoFile), nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("no go.mod above the working directory, so there is no module to find %s in; pass -proto", protoFile) + } + dir = parent + } +} + +func addChain(content, name string, selector uint64) (string, error) { + // Find defaults array + re := regexp.MustCompile(`defaults:\s*\[([\s\S]*?)\n\s*\]`) + match := re.FindStringSubmatch(content) + if len(match) < 2 { + return "", fmt.Errorf("defaults array not found") + } + + // Parse entries + entryRe := regexp.MustCompile(`\{\s*key:\s*"([^"]+)"\s*value:\s*(\d+)\s*\}`) + entries := entryRe.FindAllStringSubmatch(match[1], -1) + + type entry struct { + key string + val uint64 + } + var list []entry + for _, e := range entries { + v, _ := strconv.ParseUint(e[2], 10, 64) + list = append(list, entry{e[1], v}) + } + list = append(list, entry{name, selector}) + + sort.Slice(list, func(i, j int) bool { return list[i].key < list[j].key }) + + var b strings.Builder + b.WriteString("defaults: [\n") + for i, e := range list { + b.WriteString(fmt.Sprintf(" {\n key: \"%s\"\n value: %d\n }", e.key, e.val)) + if i < len(list)-1 { + b.WriteString(",\n") + } else { + b.WriteString("\n") + } + } + b.WriteString(" ]") + + return re.ReplaceAllString(content, b.String()), nil +} + +func fatal(format string, args ...any) { + fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...) + os.Exit(1) +} diff --git a/chain_capabilities/evm/protos/client.pb.go b/chain_capabilities/evm/protos/client.pb.go new file mode 100644 index 000000000..a03ba106e --- /dev/null +++ b/chain_capabilities/evm/protos/client.pb.go @@ -0,0 +1,2051 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: capabilities/blockchain/evm/v1alpha/client.proto + +package protos + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + + 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" +) + +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 ConfidenceLevel int32 + +const ( + ConfidenceLevel_CONFIDENCE_LEVEL_SAFE ConfidenceLevel = 0 + ConfidenceLevel_CONFIDENCE_LEVEL_LATEST ConfidenceLevel = 1 + ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED ConfidenceLevel = 2 +) + +// Enum value maps for ConfidenceLevel. +var ( + ConfidenceLevel_name = map[int32]string{ + 0: "CONFIDENCE_LEVEL_SAFE", + 1: "CONFIDENCE_LEVEL_LATEST", + 2: "CONFIDENCE_LEVEL_FINALIZED", + } + ConfidenceLevel_value = map[string]int32{ + "CONFIDENCE_LEVEL_SAFE": 0, + "CONFIDENCE_LEVEL_LATEST": 1, + "CONFIDENCE_LEVEL_FINALIZED": 2, + } +) + +func (x ConfidenceLevel) Enum() *ConfidenceLevel { + p := new(ConfidenceLevel) + *p = x + return p +} + +func (x ConfidenceLevel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfidenceLevel) Descriptor() protoreflect.EnumDescriptor { + return file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes[0].Descriptor() +} + +func (ConfidenceLevel) Type() protoreflect.EnumType { + return &file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes[0] +} + +func (x ConfidenceLevel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfidenceLevel.Descriptor instead. +func (ConfidenceLevel) EnumDescriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{0} +} + +type ReceiverContractExecutionStatus int32 + +const ( + ReceiverContractExecutionStatus_RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS ReceiverContractExecutionStatus = 0 + ReceiverContractExecutionStatus_RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED ReceiverContractExecutionStatus = 1 +) + +// Enum value maps for ReceiverContractExecutionStatus. +var ( + ReceiverContractExecutionStatus_name = map[int32]string{ + 0: "RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS", + 1: "RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED", + } + ReceiverContractExecutionStatus_value = map[string]int32{ + "RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS": 0, + "RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED": 1, + } +) + +func (x ReceiverContractExecutionStatus) Enum() *ReceiverContractExecutionStatus { + p := new(ReceiverContractExecutionStatus) + *p = x + return p +} + +func (x ReceiverContractExecutionStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ReceiverContractExecutionStatus) Descriptor() protoreflect.EnumDescriptor { + return file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes[1].Descriptor() +} + +func (ReceiverContractExecutionStatus) Type() protoreflect.EnumType { + return &file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes[1] +} + +func (x ReceiverContractExecutionStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ReceiverContractExecutionStatus.Descriptor instead. +func (ReceiverContractExecutionStatus) EnumDescriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{1} +} + +type TxStatus int32 + +const ( + TxStatus_TX_STATUS_FATAL TxStatus = 0 + TxStatus_TX_STATUS_REVERTED TxStatus = 1 + TxStatus_TX_STATUS_SUCCESS TxStatus = 2 +) + +// Enum value maps for TxStatus. +var ( + TxStatus_name = map[int32]string{ + 0: "TX_STATUS_FATAL", + 1: "TX_STATUS_REVERTED", + 2: "TX_STATUS_SUCCESS", + } + TxStatus_value = map[string]int32{ + "TX_STATUS_FATAL": 0, + "TX_STATUS_REVERTED": 1, + "TX_STATUS_SUCCESS": 2, + } +) + +func (x TxStatus) Enum() *TxStatus { + p := new(TxStatus) + *p = x + return p +} + +func (x TxStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TxStatus) Descriptor() protoreflect.EnumDescriptor { + return file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes[2].Descriptor() +} + +func (TxStatus) Type() protoreflect.EnumType { + return &file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes[2] +} + +func (x TxStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TxStatus.Descriptor instead. +func (TxStatus) EnumDescriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{2} +} + +type TopicValues struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values [][]byte `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` // list of possible values for any topic, in [32]byte fix-sized array format + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TopicValues) Reset() { + *x = TopicValues{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TopicValues) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TopicValues) ProtoMessage() {} + +func (x *TopicValues) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_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 TopicValues.ProtoReflect.Descriptor instead. +func (*TopicValues) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{0} +} + +func (x *TopicValues) GetValues() [][]byte { + if x != nil { + return x.Values + } + return nil +} + +type FilterLogTriggerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addresses [][]byte `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` // list of addresses to include in evm address [20]byte fix-sized array format, at least one address is required + // TopicValues is a fixed 4 length array of possible values for any topic where: + // a) the first element is an array of the event signatures (keccak256 of the event name and indexed args types), it has to have at least one value + // b) the second element is an array of possible values for the first indexed argument, can be empty + // c) the third element is an array of possible values for the second indexed argument, can be empty + // d) the fourth element is an array of possible values for the third indexed argument, can be empty + Topics []*TopicValues `protobuf:"bytes,2,rep,name=topics,proto3" json:"topics,omitempty"` + Confidence ConfidenceLevel `protobuf:"varint,3,opt,name=confidence,proto3,enum=capabilities.blockchain.evm.v1alpha.ConfidenceLevel" json:"confidence,omitempty"` // optional, defaults to "SAFE" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FilterLogTriggerRequest) Reset() { + *x = FilterLogTriggerRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FilterLogTriggerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FilterLogTriggerRequest) ProtoMessage() {} + +func (x *FilterLogTriggerRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_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 FilterLogTriggerRequest.ProtoReflect.Descriptor instead. +func (*FilterLogTriggerRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{1} +} + +func (x *FilterLogTriggerRequest) GetAddresses() [][]byte { + if x != nil { + return x.Addresses + } + return nil +} + +func (x *FilterLogTriggerRequest) GetTopics() []*TopicValues { + if x != nil { + return x.Topics + } + return nil +} + +func (x *FilterLogTriggerRequest) GetConfidence() ConfidenceLevel { + if x != nil { + return x.Confidence + } + return ConfidenceLevel_CONFIDENCE_LEVEL_SAFE +} + +// CallContractRequest has arguments for reading a contract as specified in the call message at a block height defined by blockNumber where: +// blockNumber : +// +// nil (default) or (-2) → use the latest mined block (“latest”) +// FinalizedBlockNumber(-3) → last finalized block (“finalized”) +// +// Any positive value is treated as an explicit block height. +type CallContractRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Call *CallMsg `protobuf:"bytes,1,opt,name=call,proto3" json:"call,omitempty"` + BlockNumber *pb.BigInt `protobuf:"bytes,2,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallContractRequest) Reset() { + *x = CallContractRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallContractRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallContractRequest) ProtoMessage() {} + +func (x *CallContractRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_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 CallContractRequest.ProtoReflect.Descriptor instead. +func (*CallContractRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{2} +} + +func (x *CallContractRequest) GetCall() *CallMsg { + if x != nil { + return x.Call + } + return nil +} + +func (x *CallContractRequest) GetBlockNumber() *pb.BigInt { + if x != nil { + return x.BlockNumber + } + return nil +} + +type CallContractReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` // solidity-spec abi encoded bytes + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallContractReply) Reset() { + *x = CallContractReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallContractReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallContractReply) ProtoMessage() {} + +func (x *CallContractReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_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 CallContractReply.ProtoReflect.Descriptor instead. +func (*CallContractReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{3} +} + +func (x *CallContractReply) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type FilterLogsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + FilterQuery *FilterQuery `protobuf:"bytes,1,opt,name=filter_query,json=filterQuery,proto3" json:"filter_query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FilterLogsRequest) Reset() { + *x = FilterLogsRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FilterLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FilterLogsRequest) ProtoMessage() {} + +func (x *FilterLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[4] + 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 FilterLogsRequest.ProtoReflect.Descriptor instead. +func (*FilterLogsRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{4} +} + +func (x *FilterLogsRequest) GetFilterQuery() *FilterQuery { + if x != nil { + return x.FilterQuery + } + return nil +} + +type FilterLogsReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Logs []*Log `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FilterLogsReply) Reset() { + *x = FilterLogsReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FilterLogsReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FilterLogsReply) ProtoMessage() {} + +func (x *FilterLogsReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[5] + 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 FilterLogsReply.ProtoReflect.Descriptor instead. +func (*FilterLogsReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{5} +} + +func (x *FilterLogsReply) GetLogs() []*Log { + if x != nil { + return x.Logs + } + return nil +} + +// represents evm-style log +type Log struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // address of the contract emitted the log in evm address [20]byte fix-sized array format + Topics [][]byte `protobuf:"bytes,2,rep,name=topics,proto3" json:"topics,omitempty"` // indexed log fields, in [32]byte fix-sized array format + TxHash []byte `protobuf:"bytes,3,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` // hash of the transaction containing the log, in [32]byte fix-sized array format + BlockHash []byte `protobuf:"bytes,4,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"` // hash of the block containing the log, in [32]byte fix-sized array format + Data []byte `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` // solidity-spec abi encoded log Data + EventSig []byte `protobuf:"bytes,6,opt,name=event_sig,json=eventSig,proto3" json:"event_sig,omitempty"` // keccak256 of event signature, in [32]byte fix-sized array format + BlockNumber *pb.BigInt `protobuf:"bytes,7,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` // block number containing the log + TxIndex uint32 `protobuf:"varint,8,opt,name=tx_index,json=txIndex,proto3" json:"tx_index,omitempty"` // index of transaction emmited the log + Index uint32 `protobuf:"varint,9,opt,name=index,proto3" json:"index,omitempty"` // index of the Log within the intire block + Removed bool `protobuf:"varint,10,opt,name=removed,proto3" json:"removed,omitempty"` // flag if the log was removed during reorg + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Log) Reset() { + *x = Log{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Log) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Log) ProtoMessage() {} + +func (x *Log) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[6] + 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 Log.ProtoReflect.Descriptor instead. +func (*Log) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{6} +} + +func (x *Log) GetAddress() []byte { + if x != nil { + return x.Address + } + return nil +} + +func (x *Log) GetTopics() [][]byte { + if x != nil { + return x.Topics + } + return nil +} + +func (x *Log) GetTxHash() []byte { + if x != nil { + return x.TxHash + } + return nil +} + +func (x *Log) GetBlockHash() []byte { + if x != nil { + return x.BlockHash + } + return nil +} + +func (x *Log) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *Log) GetEventSig() []byte { + if x != nil { + return x.EventSig + } + return nil +} + +func (x *Log) GetBlockNumber() *pb.BigInt { + if x != nil { + return x.BlockNumber + } + return nil +} + +func (x *Log) GetTxIndex() uint32 { + if x != nil { + return x.TxIndex + } + return 0 +} + +func (x *Log) GetIndex() uint32 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *Log) GetRemoved() bool { + if x != nil { + return x.Removed + } + return false +} + +// represents simplified evm-style CallMsg +type CallMsg struct { + state protoimpl.MessageState `protogen:"open.v1"` + From []byte `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` // sender address in evm address [20]byte fix-sized array format + To []byte `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` // contract address in evm address [20]byte fix-sized array format + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` // solidity-spec abi encoded bytes + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallMsg) Reset() { + *x = CallMsg{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallMsg) ProtoMessage() {} + +func (x *CallMsg) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[7] + 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 CallMsg.ProtoReflect.Descriptor instead. +func (*CallMsg) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{7} +} + +func (x *CallMsg) GetFrom() []byte { + if x != nil { + return x.From + } + return nil +} + +func (x *CallMsg) GetTo() []byte { + if x != nil { + return x.To + } + return nil +} + +func (x *CallMsg) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// represents evm-style filter query +type FilterQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + BlockHash []byte `protobuf:"bytes,1,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"` // exact block (cant use from/to), in [32]byte fix-sized array format + FromBlock *pb.BigInt `protobuf:"bytes,2,opt,name=from_block,json=fromBlock,proto3" json:"from_block,omitempty"` // start block range + ToBlock *pb.BigInt `protobuf:"bytes,3,opt,name=to_block,json=toBlock,proto3" json:"to_block,omitempty"` // end block range + Addresses [][]byte `protobuf:"bytes,4,rep,name=addresses,proto3" json:"addresses,omitempty"` // contract(s) to filter logs from in evm address [20]byte fix-sized array format + Topics []*Topics `protobuf:"bytes,5,rep,name=topics,proto3" json:"topics,omitempty"` // filter log by event signature and indexed args + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FilterQuery) Reset() { + *x = FilterQuery{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FilterQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FilterQuery) ProtoMessage() {} + +func (x *FilterQuery) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[8] + 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 FilterQuery.ProtoReflect.Descriptor instead. +func (*FilterQuery) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{8} +} + +func (x *FilterQuery) GetBlockHash() []byte { + if x != nil { + return x.BlockHash + } + return nil +} + +func (x *FilterQuery) GetFromBlock() *pb.BigInt { + if x != nil { + return x.FromBlock + } + return nil +} + +func (x *FilterQuery) GetToBlock() *pb.BigInt { + if x != nil { + return x.ToBlock + } + return nil +} + +func (x *FilterQuery) GetAddresses() [][]byte { + if x != nil { + return x.Addresses + } + return nil +} + +func (x *FilterQuery) GetTopics() []*Topics { + if x != nil { + return x.Topics + } + return nil +} + +type Topics struct { + state protoimpl.MessageState `protogen:"open.v1"` + Topic [][]byte `protobuf:"bytes,1,rep,name=topic,proto3" json:"topic,omitempty"` // in [32]byte fix-sized array format + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Topics) Reset() { + *x = Topics{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Topics) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Topics) ProtoMessage() {} + +func (x *Topics) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[9] + 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 Topics.ProtoReflect.Descriptor instead. +func (*Topics) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{9} +} + +func (x *Topics) GetTopic() [][]byte { + if x != nil { + return x.Topic + } + return nil +} + +type BalanceAtRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account []byte `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` // in evm address [20]byte fix-sized array format + BlockNumber *pb.BigInt `protobuf:"bytes,2,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BalanceAtRequest) Reset() { + *x = BalanceAtRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BalanceAtRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BalanceAtRequest) ProtoMessage() {} + +func (x *BalanceAtRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[10] + 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 BalanceAtRequest.ProtoReflect.Descriptor instead. +func (*BalanceAtRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{10} +} + +func (x *BalanceAtRequest) GetAccount() []byte { + if x != nil { + return x.Account + } + return nil +} + +func (x *BalanceAtRequest) GetBlockNumber() *pb.BigInt { + if x != nil { + return x.BlockNumber + } + return nil +} + +type BalanceAtReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Balance *pb.BigInt `protobuf:"bytes,1,opt,name=balance,proto3" json:"balance,omitempty"` // Balance of the account in wei (10^-18 eth) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BalanceAtReply) Reset() { + *x = BalanceAtReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BalanceAtReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BalanceAtReply) ProtoMessage() {} + +func (x *BalanceAtReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[11] + 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 BalanceAtReply.ProtoReflect.Descriptor instead. +func (*BalanceAtReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{11} +} + +func (x *BalanceAtReply) GetBalance() *pb.BigInt { + if x != nil { + return x.Balance + } + return nil +} + +type EstimateGasRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Msg *CallMsg `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` // simulates tx execution returns approximate amount of gas units needed + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EstimateGasRequest) Reset() { + *x = EstimateGasRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EstimateGasRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EstimateGasRequest) ProtoMessage() {} + +func (x *EstimateGasRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[12] + 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 EstimateGasRequest.ProtoReflect.Descriptor instead. +func (*EstimateGasRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{12} +} + +func (x *EstimateGasRequest) GetMsg() *CallMsg { + if x != nil { + return x.Msg + } + return nil +} + +type EstimateGasReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Gas uint64 `protobuf:"varint,1,opt,name=gas,proto3" json:"gas,omitempty"` // estimated amount of gas in gas units, needed for tx execution + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EstimateGasReply) Reset() { + *x = EstimateGasReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EstimateGasReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EstimateGasReply) ProtoMessage() {} + +func (x *EstimateGasReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[13] + 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 EstimateGasReply.ProtoReflect.Descriptor instead. +func (*EstimateGasReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{13} +} + +func (x *EstimateGasReply) GetGas() uint64 { + if x != nil { + return x.Gas + } + return 0 +} + +type GetTransactionByHashRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` // in [32]byte fix-sized array format + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransactionByHashRequest) Reset() { + *x = GetTransactionByHashRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransactionByHashRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransactionByHashRequest) ProtoMessage() {} + +func (x *GetTransactionByHashRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[14] + 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 GetTransactionByHashRequest.ProtoReflect.Descriptor instead. +func (*GetTransactionByHashRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{14} +} + +func (x *GetTransactionByHashRequest) GetHash() []byte { + if x != nil { + return x.Hash + } + return nil +} + +type GetTransactionByHashReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Transaction *Transaction `protobuf:"bytes,1,opt,name=transaction,proto3" json:"transaction,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransactionByHashReply) Reset() { + *x = GetTransactionByHashReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransactionByHashReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransactionByHashReply) ProtoMessage() {} + +func (x *GetTransactionByHashReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[15] + 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 GetTransactionByHashReply.ProtoReflect.Descriptor instead. +func (*GetTransactionByHashReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{15} +} + +func (x *GetTransactionByHashReply) GetTransaction() *Transaction { + if x != nil { + return x.Transaction + } + return nil +} + +// represents evm-style transaction +type Transaction struct { + state protoimpl.MessageState `protogen:"open.v1"` + Nonce uint64 `protobuf:"varint,1,opt,name=nonce,proto3" json:"nonce,omitempty"` // number of txs sent from sender + Gas uint64 `protobuf:"varint,2,opt,name=gas,proto3" json:"gas,omitempty"` // max gas allowed per execution (in gas units) + To []byte `protobuf:"bytes,3,opt,name=to,proto3" json:"to,omitempty"` // recipient address in evm address [20]byte fix-sized array format + Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` // solidity-spec abi encoded input data for function call payload + Hash []byte `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"` // transaction hash, in [32]byte fix-sized array format + Value *pb.BigInt `protobuf:"bytes,6,opt,name=value,proto3" json:"value,omitempty"` // amount of eth sent in wei + GasPrice *pb.BigInt `protobuf:"bytes,7,opt,name=gas_price,json=gasPrice,proto3" json:"gas_price,omitempty"` // price for a single gas unit in wei + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Transaction) Reset() { + *x = Transaction{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Transaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Transaction) ProtoMessage() {} + +func (x *Transaction) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[16] + 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 Transaction.ProtoReflect.Descriptor instead. +func (*Transaction) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{16} +} + +func (x *Transaction) GetNonce() uint64 { + if x != nil { + return x.Nonce + } + return 0 +} + +func (x *Transaction) GetGas() uint64 { + if x != nil { + return x.Gas + } + return 0 +} + +func (x *Transaction) GetTo() []byte { + if x != nil { + return x.To + } + return nil +} + +func (x *Transaction) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *Transaction) GetHash() []byte { + if x != nil { + return x.Hash + } + return nil +} + +func (x *Transaction) GetValue() *pb.BigInt { + if x != nil { + return x.Value + } + return nil +} + +func (x *Transaction) GetGasPrice() *pb.BigInt { + if x != nil { + return x.GasPrice + } + return nil +} + +type GetTransactionReceiptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` // in [32]byte fix-sized array format + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransactionReceiptRequest) Reset() { + *x = GetTransactionReceiptRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransactionReceiptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransactionReceiptRequest) ProtoMessage() {} + +func (x *GetTransactionReceiptRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[17] + 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 GetTransactionReceiptRequest.ProtoReflect.Descriptor instead. +func (*GetTransactionReceiptRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{17} +} + +func (x *GetTransactionReceiptRequest) GetHash() []byte { + if x != nil { + return x.Hash + } + return nil +} + +type GetTransactionReceiptReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Receipt *Receipt `protobuf:"bytes,1,opt,name=receipt,proto3" json:"receipt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTransactionReceiptReply) Reset() { + *x = GetTransactionReceiptReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTransactionReceiptReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTransactionReceiptReply) ProtoMessage() {} + +func (x *GetTransactionReceiptReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[18] + 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 GetTransactionReceiptReply.ProtoReflect.Descriptor instead. +func (*GetTransactionReceiptReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{18} +} + +func (x *GetTransactionReceiptReply) GetReceipt() *Receipt { + if x != nil { + return x.Receipt + } + return nil +} + +// represents evm-style receipt +type Receipt struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status uint64 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` // 1 for success 0 for failure + GasUsed uint64 `protobuf:"varint,2,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` // gas used by this transaction (in gas units) + TxIndex uint64 `protobuf:"varint,3,opt,name=tx_index,json=txIndex,proto3" json:"tx_index,omitempty"` // index of the transaction inside of the block + BlockHash []byte `protobuf:"bytes,4,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"` // block hash containing the transaction + Logs []*Log `protobuf:"bytes,6,rep,name=logs,proto3" json:"logs,omitempty"` // logs emitted by this transaction + TxHash []byte `protobuf:"bytes,7,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` // hash of the transaction this receipt is for, in [32]byte fix-sized array format + EffectiveGasPrice *pb.BigInt `protobuf:"bytes,8,opt,name=effective_gas_price,json=effectiveGasPrice,proto3" json:"effective_gas_price,omitempty"` // actual gas price paid in wei (include after EIP-1559) + BlockNumber *pb.BigInt `protobuf:"bytes,9,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` // block number containing the transaction + ContractAddress []byte `protobuf:"bytes,10,opt,name=contract_address,json=contractAddress,proto3" json:"contract_address,omitempty"` // address of the contract if this transaction created one in evm address [20]byte fix-sized array format + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Receipt) Reset() { + *x = Receipt{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Receipt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Receipt) ProtoMessage() {} + +func (x *Receipt) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[19] + 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 Receipt.ProtoReflect.Descriptor instead. +func (*Receipt) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{19} +} + +func (x *Receipt) GetStatus() uint64 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *Receipt) GetGasUsed() uint64 { + if x != nil { + return x.GasUsed + } + return 0 +} + +func (x *Receipt) GetTxIndex() uint64 { + if x != nil { + return x.TxIndex + } + return 0 +} + +func (x *Receipt) GetBlockHash() []byte { + if x != nil { + return x.BlockHash + } + return nil +} + +func (x *Receipt) GetLogs() []*Log { + if x != nil { + return x.Logs + } + return nil +} + +func (x *Receipt) GetTxHash() []byte { + if x != nil { + return x.TxHash + } + return nil +} + +func (x *Receipt) GetEffectiveGasPrice() *pb.BigInt { + if x != nil { + return x.EffectiveGasPrice + } + return nil +} + +func (x *Receipt) GetBlockNumber() *pb.BigInt { + if x != nil { + return x.BlockNumber + } + return nil +} + +func (x *Receipt) GetContractAddress() []byte { + if x != nil { + return x.ContractAddress + } + return nil +} + +// ----- Request/Reply Wrappers ----- +type HeaderByNumberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BlockNumber *pb.BigInt `protobuf:"bytes,1,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeaderByNumberRequest) Reset() { + *x = HeaderByNumberRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeaderByNumberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeaderByNumberRequest) ProtoMessage() {} + +func (x *HeaderByNumberRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[20] + 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 HeaderByNumberRequest.ProtoReflect.Descriptor instead. +func (*HeaderByNumberRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{20} +} + +func (x *HeaderByNumberRequest) GetBlockNumber() *pb.BigInt { + if x != nil { + return x.BlockNumber + } + return nil +} + +type HeaderByNumberReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Header *Header `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeaderByNumberReply) Reset() { + *x = HeaderByNumberReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeaderByNumberReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeaderByNumberReply) ProtoMessage() {} + +func (x *HeaderByNumberReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[21] + 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 HeaderByNumberReply.ProtoReflect.Descriptor instead. +func (*HeaderByNumberReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{21} +} + +func (x *HeaderByNumberReply) GetHeader() *Header { + if x != nil { + return x.Header + } + return nil +} + +type Header struct { + state protoimpl.MessageState `protogen:"open.v1"` + Timestamp uint64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // unix timestamp + BlockNumber *pb.BigInt `protobuf:"bytes,2,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` + Hash []byte `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"` // in [32]byte fix-sized array format + ParentHash []byte `protobuf:"bytes,4,opt,name=parent_hash,json=parentHash,proto3" json:"parent_hash,omitempty"` // in [32]byte fix-sized array format + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Header) Reset() { + *x = Header{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[22] + 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 Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{22} +} + +func (x *Header) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *Header) GetBlockNumber() *pb.BigInt { + if x != nil { + return x.BlockNumber + } + return nil +} + +func (x *Header) GetHash() []byte { + if x != nil { + return x.Hash + } + return nil +} + +func (x *Header) GetParentHash() []byte { + if x != nil { + return x.ParentHash + } + return nil +} + +type WriteReportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Receiver []byte `protobuf:"bytes,1,opt,name=receiver,proto3" json:"receiver,omitempty"` + Report *sdk.ReportResponse `protobuf:"bytes,2,opt,name=report,proto3" json:"report,omitempty"` + GasConfig *GasConfig `protobuf:"bytes,3,opt,name=gas_config,json=gasConfig,proto3,oneof" json:"gas_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteReportRequest) Reset() { + *x = WriteReportRequest{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteReportRequest) ProtoMessage() {} + +func (x *WriteReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[23] + 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 WriteReportRequest.ProtoReflect.Descriptor instead. +func (*WriteReportRequest) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{23} +} + +func (x *WriteReportRequest) GetReceiver() []byte { + if x != nil { + return x.Receiver + } + return nil +} + +func (x *WriteReportRequest) GetReport() *sdk.ReportResponse { + if x != nil { + return x.Report + } + return nil +} + +func (x *WriteReportRequest) GetGasConfig() *GasConfig { + if x != nil { + return x.GasConfig + } + return nil +} + +type GasConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + GasLimit uint64 `protobuf:"varint,1,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GasConfig) Reset() { + *x = GasConfig{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GasConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GasConfig) ProtoMessage() {} + +func (x *GasConfig) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[24] + 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 GasConfig.ProtoReflect.Descriptor instead. +func (*GasConfig) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{24} +} + +func (x *GasConfig) GetGasLimit() uint64 { + if x != nil { + return x.GasLimit + } + return 0 +} + +type WriteReportReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + TxStatus TxStatus `protobuf:"varint,1,opt,name=tx_status,json=txStatus,proto3,enum=capabilities.blockchain.evm.v1alpha.TxStatus" json:"tx_status,omitempty"` + ReceiverContractExecutionStatus *ReceiverContractExecutionStatus `protobuf:"varint,2,opt,name=receiver_contract_execution_status,json=receiverContractExecutionStatus,proto3,enum=capabilities.blockchain.evm.v1alpha.ReceiverContractExecutionStatus,oneof" json:"receiver_contract_execution_status,omitempty"` + TxHash []byte `protobuf:"bytes,3,opt,name=tx_hash,json=txHash,proto3,oneof" json:"tx_hash,omitempty"` + TransactionFee *pb.BigInt `protobuf:"bytes,4,opt,name=transaction_fee,json=transactionFee,proto3,oneof" json:"transaction_fee,omitempty"` + ErrorMessage *string `protobuf:"bytes,5,opt,name=error_message,json=errorMessage,proto3,oneof" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteReportReply) Reset() { + *x = WriteReportReply{} + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteReportReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteReportReply) ProtoMessage() {} + +func (x *WriteReportReply) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[25] + 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 WriteReportReply.ProtoReflect.Descriptor instead. +func (*WriteReportReply) Descriptor() ([]byte, []int) { + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP(), []int{25} +} + +func (x *WriteReportReply) GetTxStatus() TxStatus { + if x != nil { + return x.TxStatus + } + return TxStatus_TX_STATUS_FATAL +} + +func (x *WriteReportReply) GetReceiverContractExecutionStatus() ReceiverContractExecutionStatus { + if x != nil && x.ReceiverContractExecutionStatus != nil { + return *x.ReceiverContractExecutionStatus + } + return ReceiverContractExecutionStatus_RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS +} + +func (x *WriteReportReply) GetTxHash() []byte { + if x != nil { + return x.TxHash + } + return nil +} + +func (x *WriteReportReply) GetTransactionFee() *pb.BigInt { + if x != nil { + return x.TransactionFee + } + return nil +} + +func (x *WriteReportReply) GetErrorMessage() string { + if x != nil && x.ErrorMessage != nil { + return *x.ErrorMessage + } + return "" +} + +var File_capabilities_blockchain_evm_v1alpha_client_proto protoreflect.FileDescriptor + +const file_capabilities_blockchain_evm_v1alpha_client_proto_rawDesc = "" + + "\n" + + "0capabilities/blockchain/evm/v1alpha/client.proto\x12#capabilities.blockchain.evm.v1alpha\x1a\x15sdk/v1alpha/sdk.proto\x1a*tools/generator/v1alpha/cre_metadata.proto\x1a\x16values/v1/values.proto\"%\n" + + "\vTopicValues\x12\x16\n" + + "\x06values\x18\x01 \x03(\fR\x06values\"\xd7\x01\n" + + "\x17FilterLogTriggerRequest\x12\x1c\n" + + "\taddresses\x18\x01 \x03(\fR\taddresses\x12H\n" + + "\x06topics\x18\x02 \x03(\v20.capabilities.blockchain.evm.v1alpha.TopicValuesR\x06topics\x12T\n" + + "\n" + + "confidence\x18\x03 \x01(\x0e24.capabilities.blockchain.evm.v1alpha.ConfidenceLevelR\n" + + "confidence\"\x8d\x01\n" + + "\x13CallContractRequest\x12@\n" + + "\x04call\x18\x01 \x01(\v2,.capabilities.blockchain.evm.v1alpha.CallMsgR\x04call\x124\n" + + "\fblock_number\x18\x02 \x01(\v2\x11.values.v1.BigIntR\vblockNumber\"'\n" + + "\x11CallContractReply\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"h\n" + + "\x11FilterLogsRequest\x12S\n" + + "\ffilter_query\x18\x01 \x01(\v20.capabilities.blockchain.evm.v1alpha.FilterQueryR\vfilterQuery\"O\n" + + "\x0fFilterLogsReply\x12<\n" + + "\x04logs\x18\x01 \x03(\v2(.capabilities.blockchain.evm.v1alpha.LogR\x04logs\"\xa1\x02\n" + + "\x03Log\x12\x18\n" + + "\aaddress\x18\x01 \x01(\fR\aaddress\x12\x16\n" + + "\x06topics\x18\x02 \x03(\fR\x06topics\x12\x17\n" + + "\atx_hash\x18\x03 \x01(\fR\x06txHash\x12\x1d\n" + + "\n" + + "block_hash\x18\x04 \x01(\fR\tblockHash\x12\x12\n" + + "\x04data\x18\x05 \x01(\fR\x04data\x12\x1b\n" + + "\tevent_sig\x18\x06 \x01(\fR\beventSig\x124\n" + + "\fblock_number\x18\a \x01(\v2\x11.values.v1.BigIntR\vblockNumber\x12\x19\n" + + "\btx_index\x18\b \x01(\rR\atxIndex\x12\x14\n" + + "\x05index\x18\t \x01(\rR\x05index\x12\x18\n" + + "\aremoved\x18\n" + + " \x01(\bR\aremoved\"A\n" + + "\aCallMsg\x12\x12\n" + + "\x04from\x18\x01 \x01(\fR\x04from\x12\x0e\n" + + "\x02to\x18\x02 \x01(\fR\x02to\x12\x12\n" + + "\x04data\x18\x03 \x01(\fR\x04data\"\xef\x01\n" + + "\vFilterQuery\x12\x1d\n" + + "\n" + + "block_hash\x18\x01 \x01(\fR\tblockHash\x120\n" + + "\n" + + "from_block\x18\x02 \x01(\v2\x11.values.v1.BigIntR\tfromBlock\x12,\n" + + "\bto_block\x18\x03 \x01(\v2\x11.values.v1.BigIntR\atoBlock\x12\x1c\n" + + "\taddresses\x18\x04 \x03(\fR\taddresses\x12C\n" + + "\x06topics\x18\x05 \x03(\v2+.capabilities.blockchain.evm.v1alpha.TopicsR\x06topics\"\x1e\n" + + "\x06Topics\x12\x14\n" + + "\x05topic\x18\x01 \x03(\fR\x05topic\"b\n" + + "\x10BalanceAtRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\fR\aaccount\x124\n" + + "\fblock_number\x18\x02 \x01(\v2\x11.values.v1.BigIntR\vblockNumber\"=\n" + + "\x0eBalanceAtReply\x12+\n" + + "\abalance\x18\x01 \x01(\v2\x11.values.v1.BigIntR\abalance\"T\n" + + "\x12EstimateGasRequest\x12>\n" + + "\x03msg\x18\x01 \x01(\v2,.capabilities.blockchain.evm.v1alpha.CallMsgR\x03msg\"$\n" + + "\x10EstimateGasReply\x12\x10\n" + + "\x03gas\x18\x01 \x01(\x04R\x03gas\"1\n" + + "\x1bGetTransactionByHashRequest\x12\x12\n" + + "\x04hash\x18\x01 \x01(\fR\x04hash\"o\n" + + "\x19GetTransactionByHashReply\x12R\n" + + "\vtransaction\x18\x01 \x01(\v20.capabilities.blockchain.evm.v1alpha.TransactionR\vtransaction\"\xc6\x01\n" + + "\vTransaction\x12\x14\n" + + "\x05nonce\x18\x01 \x01(\x04R\x05nonce\x12\x10\n" + + "\x03gas\x18\x02 \x01(\x04R\x03gas\x12\x0e\n" + + "\x02to\x18\x03 \x01(\fR\x02to\x12\x12\n" + + "\x04data\x18\x04 \x01(\fR\x04data\x12\x12\n" + + "\x04hash\x18\x05 \x01(\fR\x04hash\x12'\n" + + "\x05value\x18\x06 \x01(\v2\x11.values.v1.BigIntR\x05value\x12.\n" + + "\tgas_price\x18\a \x01(\v2\x11.values.v1.BigIntR\bgasPrice\"2\n" + + "\x1cGetTransactionReceiptRequest\x12\x12\n" + + "\x04hash\x18\x01 \x01(\fR\x04hash\"d\n" + + "\x1aGetTransactionReceiptReply\x12F\n" + + "\areceipt\x18\x01 \x01(\v2,.capabilities.blockchain.evm.v1alpha.ReceiptR\areceipt\"\xf1\x02\n" + + "\aReceipt\x12\x16\n" + + "\x06status\x18\x01 \x01(\x04R\x06status\x12\x19\n" + + "\bgas_used\x18\x02 \x01(\x04R\agasUsed\x12\x19\n" + + "\btx_index\x18\x03 \x01(\x04R\atxIndex\x12\x1d\n" + + "\n" + + "block_hash\x18\x04 \x01(\fR\tblockHash\x12<\n" + + "\x04logs\x18\x06 \x03(\v2(.capabilities.blockchain.evm.v1alpha.LogR\x04logs\x12\x17\n" + + "\atx_hash\x18\a \x01(\fR\x06txHash\x12A\n" + + "\x13effective_gas_price\x18\b \x01(\v2\x11.values.v1.BigIntR\x11effectiveGasPrice\x124\n" + + "\fblock_number\x18\t \x01(\v2\x11.values.v1.BigIntR\vblockNumber\x12)\n" + + "\x10contract_address\x18\n" + + " \x01(\fR\x0fcontractAddress\"M\n" + + "\x15HeaderByNumberRequest\x124\n" + + "\fblock_number\x18\x01 \x01(\v2\x11.values.v1.BigIntR\vblockNumber\"Z\n" + + "\x13HeaderByNumberReply\x12C\n" + + "\x06header\x18\x01 \x01(\v2+.capabilities.blockchain.evm.v1alpha.HeaderR\x06header\"\x91\x01\n" + + "\x06Header\x12\x1c\n" + + "\ttimestamp\x18\x01 \x01(\x04R\ttimestamp\x124\n" + + "\fblock_number\x18\x02 \x01(\v2\x11.values.v1.BigIntR\vblockNumber\x12\x12\n" + + "\x04hash\x18\x03 \x01(\fR\x04hash\x12\x1f\n" + + "\vparent_hash\x18\x04 \x01(\fR\n" + + "parentHash\"\xc8\x01\n" + + "\x12WriteReportRequest\x12\x1a\n" + + "\breceiver\x18\x01 \x01(\fR\breceiver\x123\n" + + "\x06report\x18\x02 \x01(\v2\x1b.sdk.v1alpha.ReportResponseR\x06report\x12R\n" + + "\n" + + "gas_config\x18\x03 \x01(\v2..capabilities.blockchain.evm.v1alpha.GasConfigH\x00R\tgasConfig\x88\x01\x01B\r\n" + + "\v_gas_config\"(\n" + + "\tGasConfig\x12\x1b\n" + + "\tgas_limit\x18\x01 \x01(\x04R\bgasLimit\"\xd9\x03\n" + + "\x10WriteReportReply\x12J\n" + + "\ttx_status\x18\x01 \x01(\x0e2-.capabilities.blockchain.evm.v1alpha.TxStatusR\btxStatus\x12\x96\x01\n" + + "\"receiver_contract_execution_status\x18\x02 \x01(\x0e2D.capabilities.blockchain.evm.v1alpha.ReceiverContractExecutionStatusH\x00R\x1freceiverContractExecutionStatus\x88\x01\x01\x12\x1c\n" + + "\atx_hash\x18\x03 \x01(\fH\x01R\x06txHash\x88\x01\x01\x12?\n" + + "\x0ftransaction_fee\x18\x04 \x01(\v2\x11.values.v1.BigIntH\x02R\x0etransactionFee\x88\x01\x01\x12(\n" + + "\rerror_message\x18\x05 \x01(\tH\x03R\ferrorMessage\x88\x01\x01B%\n" + + "#_receiver_contract_execution_statusB\n" + + "\n" + + "\b_tx_hashB\x12\n" + + "\x10_transaction_feeB\x10\n" + + "\x0e_error_message*i\n" + + "\x0fConfidenceLevel\x12\x19\n" + + "\x15CONFIDENCE_LEVEL_SAFE\x10\x00\x12\x1b\n" + + "\x17CONFIDENCE_LEVEL_LATEST\x10\x01\x12\x1e\n" + + "\x1aCONFIDENCE_LEVEL_FINALIZED\x10\x02*\x82\x01\n" + + "\x1fReceiverContractExecutionStatus\x12.\n" + + "*RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS\x10\x00\x12/\n" + + "+RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED\x10\x01*N\n" + + "\bTxStatus\x12\x13\n" + + "\x0fTX_STATUS_FATAL\x10\x00\x12\x16\n" + + "\x12TX_STATUS_REVERTED\x10\x01\x12\x15\n" + + "\x11TX_STATUS_SUCCESS\x10\x022\xe7\x19\n" + + "\x06Client\x12\x80\x01\n" + + "\fCallContract\x128.capabilities.blockchain.evm.v1alpha.CallContractRequest\x1a6.capabilities.blockchain.evm.v1alpha.CallContractReply\x12z\n" + + "\n" + + "FilterLogs\x126.capabilities.blockchain.evm.v1alpha.FilterLogsRequest\x1a4.capabilities.blockchain.evm.v1alpha.FilterLogsReply\x12w\n" + + "\tBalanceAt\x125.capabilities.blockchain.evm.v1alpha.BalanceAtRequest\x1a3.capabilities.blockchain.evm.v1alpha.BalanceAtReply\x12}\n" + + "\vEstimateGas\x127.capabilities.blockchain.evm.v1alpha.EstimateGasRequest\x1a5.capabilities.blockchain.evm.v1alpha.EstimateGasReply\x12\x98\x01\n" + + "\x14GetTransactionByHash\x12@.capabilities.blockchain.evm.v1alpha.GetTransactionByHashRequest\x1a>.capabilities.blockchain.evm.v1alpha.GetTransactionByHashReply\x12\x9b\x01\n" + + "\x15GetTransactionReceipt\x12A.capabilities.blockchain.evm.v1alpha.GetTransactionReceiptRequest\x1a?.capabilities.blockchain.evm.v1alpha.GetTransactionReceiptReply\x12\x86\x01\n" + + "\x0eHeaderByNumber\x12:.capabilities.blockchain.evm.v1alpha.HeaderByNumberRequest\x1a8.capabilities.blockchain.evm.v1alpha.HeaderByNumberReply\x12v\n" + + "\n" + + "LogTrigger\x12<.capabilities.blockchain.evm.v1alpha.FilterLogTriggerRequest\x1a(.capabilities.blockchain.evm.v1alpha.Log0\x01\x12}\n" + + "\vWriteReport\x127.capabilities.blockchain.evm.v1alpha.WriteReportRequest\x1a5.capabilities.blockchain.evm.v1alpha.WriteReportReply\x1a\xac\x10\x82\xb5\x18\xa7\x10\b\x01\x12\tevm@1.0.0\x1a\x97\x10\n" + + "\rChainSelector\x12\x85\x10\x12\x82\x10\n" + + "\x17\n" + + "\vadi-mainnet\x10\xfc\xf0\xe6·\xe7ݪ8\n" + + "\x18\n" + + "\vadi-testnet\x10\xfd\xa6\xc8\xfa\xf9\x86\x8cڂ\x01\n" + + "$\n" + + "\x17apechain-testnet-curtis\x10\xc1ô\xf8\x8dĒ\xb2\x89\x01\n" + + "\x17\n" + + "\varc-testnet\x10\xe7ƌ\x9e\xd7\xd7Ѝ*\n" + + "\x1d\n" + + "\x11avalanche-mainnet\x10\xd5\xe7\x8a\xc0\xe1\u0558\xa4Y\n" + + "#\n" + + "\x16avalanche-testnet-fuji\x10\x9b\xf9\xfc\x90\xa2\xe3\xa8\xf8\xcc\x01\n" + + "(\n" + + "\x1bbinance_smart_chain-mainnet\x10\xcf\xf7\x94\xf1\xd8핸\x9d\x01\n" + + "(\n" + + "\x1bbinance_smart_chain-testnet\x10\xfb\xad\xbe\x9c\x80\xae䊸\x01\n" + + "\x18\n" + + "\fcelo-mainnet\x10\x86\xd4\xe8؆\x93\x88\xd7\x12\n" + + "\x18\n" + + "\fcelo-sepolia\x10\xc4\xc3\xfc\x8b\xfc睚4\n" + + "\x1a\n" + + "\x0ecronos-testnet\x10\xfd\xd9\xee\xad\xe0\xde\xda\xc8)\n" + + "\"\n" + + "\x15dtcc-mainnet-appchain\x10\xd4Ա\xe3\u05fb\x8a\xce\xc0\x01\n" + + "\"\n" + + "\x15dtcc-testnet-andesite\x10҃\xe3Й\x96\xe5\xa4\xd7\x01\n" + + "\x1c\n" + + "\x10ethereum-mainnet\x10\x95\xf6\xf1\xe4ϲ\xa6\xc2E\n" + + "'\n" + + "\x1bethereum-mainnet-arbitrum-1\x10\xc4\xe8\x8d͎\x9b\xa1\xd7D\n" + + "$\n" + + "\x17ethereum-mainnet-base-1\x10\x82\xff\xab\xa2\xfe\xb9\x90\xd3\xdd\x01\n" + + "\"\n" + + "\x16ethereum-mainnet-ink-1\x10\xa0\xb0\xa6\xe9\xb7檄0\n" + + "$\n" + + "\x18ethereum-mainnet-linea-1\x10\xb6\xba\xe9\x98˽\xb0\x9b@\n" + + "%\n" + + "\x19ethereum-mainnet-mantle-1\x10\x8a紕簃\xcc\x15\n" + + "'\n" + + "\x1bethereum-mainnet-optimism-1\x10\xb8\x95\x8f\xc3\xf7\xfe\xd0\xe93\n" + + "&\n" + + "\x19ethereum-mainnet-scroll-1\x10\xb8\xbc\xe4\xebľȟ\xb7\x01\n" + + ")\n" + + "\x1dethereum-mainnet-worldchain-1\x10\x87ﺷŶ¸\x1c\n" + + "%\n" + + "\x19ethereum-mainnet-xlayer-1\x10\x96\xa5\xfc\x9c\xa6\xa8\xef\xed)\n" + + "%\n" + + "\x19ethereum-mainnet-zksync-1\x10\x94\xee\x97\xd9\xed\xb4\xb1\xd7\x15\n" + + "%\n" + + "\x18ethereum-testnet-sepolia\x10ٵ\xe4\xce\xfc\xc9\xee\xa0\xde\x01\n" + + "/\n" + + "#ethereum-testnet-sepolia-arbitrum-1\x10\xea\xce\xee\xff궄\xa30\n" + + ",\n" + + "\x1fethereum-testnet-sepolia-base-1\x10\xb8ʹ\xef\xf6\x90\xaeȏ\x01\n" + + ",\n" + + " ethereum-testnet-sepolia-linea-1\x10\xeb\xaa\xd4\xfe\x82\xf9\xe6\xafO\n" + + "-\n" + + "!ethereum-testnet-sepolia-mantle-1\x10\xd5Ƹ\xee\xcd\xf6\xf2\xa6r\n" + + "/\n" + + "#ethereum-testnet-sepolia-optimism-1\x10\x9f\x86š\xbe\xd8\xc3\xc0H\n" + + "-\n" + + "!ethereum-testnet-sepolia-scroll-1\x10\x8b鴾ۺ\xed\xd1\x1f\n" + + "0\n" + + "#ethereum-testnet-sepolia-unichain-1\x10\xb4\xde\xfe\xe0엩\x96\xc4\x01\n" + + "1\n" + + "%ethereum-testnet-sepolia-worldchain-1\x10\xba\xdf\xe0\xc5ǩ\xf3\xc5I\n" + + "-\n" + + "!ethereum-testnet-sepolia-zksync-1\x10\xb7\xc1\xfc\xfd\xf2Ā\xde_\n" + + " \n" + + "\x14gnosis_chain-mainnet\x10\xf4\x92\xad\xda\U000a2bba\x06\n" + + "'\n" + + "\x1bgnosis_chain-testnet-chiado\x10\xb3\xb1\x82Л\xa5\x8f\x8f{\n" + + "\x1f\n" + + "\x13hyperliquid-mainnet\x10\xa7\xb3\xf8\xdd\xce\xd1\xe9\xf2!\n" + + "\x1f\n" + + "\x13hyperliquid-testnet\x10\x88\xce\xddȗ\xe0ɽ;\n" + + " \n" + + "\x13ink-testnet-sepolia\x10\xe8\xf4\xa7\xa5\xf3\xe6\x96\xc0\x87\x01\n" + + "\x19\n" + + "\rjovay-mainnet\x10\xb5\xc3Ě\xa1\x80ߒ\x15\n" + + "\x19\n" + + "\rjovay-testnet\x10\xe4ϊ\x84\u07b2ގ\r\n" + + "\x1b\n" + + "\x0fmegaeth-mainnet\x10ꕶȼ\xe4\xa6\xc8T\n" + + "\x1e\n" + + "\x11megaeth-testnet-2\x10\xe3\x8dވ\xb1\x8f\xfd\x93\xfd\x01\n" + + "$\n" + + "\x17pharos-atlantic-testnet\x10̙\xed\xe0μ\xaf\xb4\xdf\x01\n" + + "\x1a\n" + + "\x0epharos-mainnet\x10\xc8\xc1\x87\x9e\xf5\xef͡l\n" + + "\x1b\n" + + "\x0eplasma-mainnet\x10\xf8\x9b\xf1\xd1\xda\xc9\xd5Ɓ\x01\n" + + "\x1a\n" + + "\x0eplasma-testnet\x10՛\xbf\xa5ô\x99\x877\n" + + "\x1b\n" + + "\x0fpolygon-mainnet\x10\xb1\xab\xe4\U0001a486\x9d8\n" + + "!\n" + + "\x14polygon-testnet-amoy\x10͏\xd6\xdf\xf1ǐ\xfa\xe1\x01\n" + + "$\n" + + "\x18private-testnet-andesite\x10Ԧ\x98\xa5\xc1\x8f\xdc\xfc_\n" + + "\"\n" + + "\x16private-testnet-pumice\x10\xf9\xc2Ķĥ\xc4\xdb\x15\n" + + "%\n" + + "\x19private-testnet-quartzite\x10\xf9\xf0\xa2ݬ݇\xfa9\n" + + "$\n" + + "\x18private-testnet-rhyolite\x10\x81\xfa\x89\xeb\xe1\xb4۱\b\n" + + "\x19\n" + + "\rsonic-mainnet\x10Ѳ\xe5\xed٠\xb2\x9d\x17\n" + + "\x19\n" + + "\rsonic-testnet\x10Ȉ\xfbԴ\xc6\xfa\xbc\x18\n" + + "\x18\n" + + "\vtac-testnet\x10\xd5ۍ\xe3\xfb\x9f\x93׃\x01\n" + + "\x1b\n" + + "\x0exlayer-testnet\x10ɾ\xa1\xb4\xad̼ݍ\x01b\x06proto3" + +var ( + file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescOnce sync.Once + file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescData []byte +) + +func file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescGZIP() []byte { + file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescOnce.Do(func() { + file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_capabilities_blockchain_evm_v1alpha_client_proto_rawDesc), len(file_capabilities_blockchain_evm_v1alpha_client_proto_rawDesc))) + }) + return file_capabilities_blockchain_evm_v1alpha_client_proto_rawDescData +} + +var file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_capabilities_blockchain_evm_v1alpha_client_proto_goTypes = []any{ + (ConfidenceLevel)(0), // 0: capabilities.blockchain.evm.v1alpha.ConfidenceLevel + (ReceiverContractExecutionStatus)(0), // 1: capabilities.blockchain.evm.v1alpha.ReceiverContractExecutionStatus + (TxStatus)(0), // 2: capabilities.blockchain.evm.v1alpha.TxStatus + (*TopicValues)(nil), // 3: capabilities.blockchain.evm.v1alpha.TopicValues + (*FilterLogTriggerRequest)(nil), // 4: capabilities.blockchain.evm.v1alpha.FilterLogTriggerRequest + (*CallContractRequest)(nil), // 5: capabilities.blockchain.evm.v1alpha.CallContractRequest + (*CallContractReply)(nil), // 6: capabilities.blockchain.evm.v1alpha.CallContractReply + (*FilterLogsRequest)(nil), // 7: capabilities.blockchain.evm.v1alpha.FilterLogsRequest + (*FilterLogsReply)(nil), // 8: capabilities.blockchain.evm.v1alpha.FilterLogsReply + (*Log)(nil), // 9: capabilities.blockchain.evm.v1alpha.Log + (*CallMsg)(nil), // 10: capabilities.blockchain.evm.v1alpha.CallMsg + (*FilterQuery)(nil), // 11: capabilities.blockchain.evm.v1alpha.FilterQuery + (*Topics)(nil), // 12: capabilities.blockchain.evm.v1alpha.Topics + (*BalanceAtRequest)(nil), // 13: capabilities.blockchain.evm.v1alpha.BalanceAtRequest + (*BalanceAtReply)(nil), // 14: capabilities.blockchain.evm.v1alpha.BalanceAtReply + (*EstimateGasRequest)(nil), // 15: capabilities.blockchain.evm.v1alpha.EstimateGasRequest + (*EstimateGasReply)(nil), // 16: capabilities.blockchain.evm.v1alpha.EstimateGasReply + (*GetTransactionByHashRequest)(nil), // 17: capabilities.blockchain.evm.v1alpha.GetTransactionByHashRequest + (*GetTransactionByHashReply)(nil), // 18: capabilities.blockchain.evm.v1alpha.GetTransactionByHashReply + (*Transaction)(nil), // 19: capabilities.blockchain.evm.v1alpha.Transaction + (*GetTransactionReceiptRequest)(nil), // 20: capabilities.blockchain.evm.v1alpha.GetTransactionReceiptRequest + (*GetTransactionReceiptReply)(nil), // 21: capabilities.blockchain.evm.v1alpha.GetTransactionReceiptReply + (*Receipt)(nil), // 22: capabilities.blockchain.evm.v1alpha.Receipt + (*HeaderByNumberRequest)(nil), // 23: capabilities.blockchain.evm.v1alpha.HeaderByNumberRequest + (*HeaderByNumberReply)(nil), // 24: capabilities.blockchain.evm.v1alpha.HeaderByNumberReply + (*Header)(nil), // 25: capabilities.blockchain.evm.v1alpha.Header + (*WriteReportRequest)(nil), // 26: capabilities.blockchain.evm.v1alpha.WriteReportRequest + (*GasConfig)(nil), // 27: capabilities.blockchain.evm.v1alpha.GasConfig + (*WriteReportReply)(nil), // 28: capabilities.blockchain.evm.v1alpha.WriteReportReply + (*pb.BigInt)(nil), // 29: values.v1.BigInt + (*sdk.ReportResponse)(nil), // 30: sdk.v1alpha.ReportResponse +} +var file_capabilities_blockchain_evm_v1alpha_client_proto_depIdxs = []int32{ + 3, // 0: capabilities.blockchain.evm.v1alpha.FilterLogTriggerRequest.topics:type_name -> capabilities.blockchain.evm.v1alpha.TopicValues + 0, // 1: capabilities.blockchain.evm.v1alpha.FilterLogTriggerRequest.confidence:type_name -> capabilities.blockchain.evm.v1alpha.ConfidenceLevel + 10, // 2: capabilities.blockchain.evm.v1alpha.CallContractRequest.call:type_name -> capabilities.blockchain.evm.v1alpha.CallMsg + 29, // 3: capabilities.blockchain.evm.v1alpha.CallContractRequest.block_number:type_name -> values.v1.BigInt + 11, // 4: capabilities.blockchain.evm.v1alpha.FilterLogsRequest.filter_query:type_name -> capabilities.blockchain.evm.v1alpha.FilterQuery + 9, // 5: capabilities.blockchain.evm.v1alpha.FilterLogsReply.logs:type_name -> capabilities.blockchain.evm.v1alpha.Log + 29, // 6: capabilities.blockchain.evm.v1alpha.Log.block_number:type_name -> values.v1.BigInt + 29, // 7: capabilities.blockchain.evm.v1alpha.FilterQuery.from_block:type_name -> values.v1.BigInt + 29, // 8: capabilities.blockchain.evm.v1alpha.FilterQuery.to_block:type_name -> values.v1.BigInt + 12, // 9: capabilities.blockchain.evm.v1alpha.FilterQuery.topics:type_name -> capabilities.blockchain.evm.v1alpha.Topics + 29, // 10: capabilities.blockchain.evm.v1alpha.BalanceAtRequest.block_number:type_name -> values.v1.BigInt + 29, // 11: capabilities.blockchain.evm.v1alpha.BalanceAtReply.balance:type_name -> values.v1.BigInt + 10, // 12: capabilities.blockchain.evm.v1alpha.EstimateGasRequest.msg:type_name -> capabilities.blockchain.evm.v1alpha.CallMsg + 19, // 13: capabilities.blockchain.evm.v1alpha.GetTransactionByHashReply.transaction:type_name -> capabilities.blockchain.evm.v1alpha.Transaction + 29, // 14: capabilities.blockchain.evm.v1alpha.Transaction.value:type_name -> values.v1.BigInt + 29, // 15: capabilities.blockchain.evm.v1alpha.Transaction.gas_price:type_name -> values.v1.BigInt + 22, // 16: capabilities.blockchain.evm.v1alpha.GetTransactionReceiptReply.receipt:type_name -> capabilities.blockchain.evm.v1alpha.Receipt + 9, // 17: capabilities.blockchain.evm.v1alpha.Receipt.logs:type_name -> capabilities.blockchain.evm.v1alpha.Log + 29, // 18: capabilities.blockchain.evm.v1alpha.Receipt.effective_gas_price:type_name -> values.v1.BigInt + 29, // 19: capabilities.blockchain.evm.v1alpha.Receipt.block_number:type_name -> values.v1.BigInt + 29, // 20: capabilities.blockchain.evm.v1alpha.HeaderByNumberRequest.block_number:type_name -> values.v1.BigInt + 25, // 21: capabilities.blockchain.evm.v1alpha.HeaderByNumberReply.header:type_name -> capabilities.blockchain.evm.v1alpha.Header + 29, // 22: capabilities.blockchain.evm.v1alpha.Header.block_number:type_name -> values.v1.BigInt + 30, // 23: capabilities.blockchain.evm.v1alpha.WriteReportRequest.report:type_name -> sdk.v1alpha.ReportResponse + 27, // 24: capabilities.blockchain.evm.v1alpha.WriteReportRequest.gas_config:type_name -> capabilities.blockchain.evm.v1alpha.GasConfig + 2, // 25: capabilities.blockchain.evm.v1alpha.WriteReportReply.tx_status:type_name -> capabilities.blockchain.evm.v1alpha.TxStatus + 1, // 26: capabilities.blockchain.evm.v1alpha.WriteReportReply.receiver_contract_execution_status:type_name -> capabilities.blockchain.evm.v1alpha.ReceiverContractExecutionStatus + 29, // 27: capabilities.blockchain.evm.v1alpha.WriteReportReply.transaction_fee:type_name -> values.v1.BigInt + 5, // 28: capabilities.blockchain.evm.v1alpha.Client.CallContract:input_type -> capabilities.blockchain.evm.v1alpha.CallContractRequest + 7, // 29: capabilities.blockchain.evm.v1alpha.Client.FilterLogs:input_type -> capabilities.blockchain.evm.v1alpha.FilterLogsRequest + 13, // 30: capabilities.blockchain.evm.v1alpha.Client.BalanceAt:input_type -> capabilities.blockchain.evm.v1alpha.BalanceAtRequest + 15, // 31: capabilities.blockchain.evm.v1alpha.Client.EstimateGas:input_type -> capabilities.blockchain.evm.v1alpha.EstimateGasRequest + 17, // 32: capabilities.blockchain.evm.v1alpha.Client.GetTransactionByHash:input_type -> capabilities.blockchain.evm.v1alpha.GetTransactionByHashRequest + 20, // 33: capabilities.blockchain.evm.v1alpha.Client.GetTransactionReceipt:input_type -> capabilities.blockchain.evm.v1alpha.GetTransactionReceiptRequest + 23, // 34: capabilities.blockchain.evm.v1alpha.Client.HeaderByNumber:input_type -> capabilities.blockchain.evm.v1alpha.HeaderByNumberRequest + 4, // 35: capabilities.blockchain.evm.v1alpha.Client.LogTrigger:input_type -> capabilities.blockchain.evm.v1alpha.FilterLogTriggerRequest + 26, // 36: capabilities.blockchain.evm.v1alpha.Client.WriteReport:input_type -> capabilities.blockchain.evm.v1alpha.WriteReportRequest + 6, // 37: capabilities.blockchain.evm.v1alpha.Client.CallContract:output_type -> capabilities.blockchain.evm.v1alpha.CallContractReply + 8, // 38: capabilities.blockchain.evm.v1alpha.Client.FilterLogs:output_type -> capabilities.blockchain.evm.v1alpha.FilterLogsReply + 14, // 39: capabilities.blockchain.evm.v1alpha.Client.BalanceAt:output_type -> capabilities.blockchain.evm.v1alpha.BalanceAtReply + 16, // 40: capabilities.blockchain.evm.v1alpha.Client.EstimateGas:output_type -> capabilities.blockchain.evm.v1alpha.EstimateGasReply + 18, // 41: capabilities.blockchain.evm.v1alpha.Client.GetTransactionByHash:output_type -> capabilities.blockchain.evm.v1alpha.GetTransactionByHashReply + 21, // 42: capabilities.blockchain.evm.v1alpha.Client.GetTransactionReceipt:output_type -> capabilities.blockchain.evm.v1alpha.GetTransactionReceiptReply + 24, // 43: capabilities.blockchain.evm.v1alpha.Client.HeaderByNumber:output_type -> capabilities.blockchain.evm.v1alpha.HeaderByNumberReply + 9, // 44: capabilities.blockchain.evm.v1alpha.Client.LogTrigger:output_type -> capabilities.blockchain.evm.v1alpha.Log + 28, // 45: capabilities.blockchain.evm.v1alpha.Client.WriteReport:output_type -> capabilities.blockchain.evm.v1alpha.WriteReportReply + 37, // [37:46] is the sub-list for method output_type + 28, // [28:37] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 28, // [28:28] is the sub-list for extension extendee + 0, // [0:28] is the sub-list for field type_name +} + +func init() { file_capabilities_blockchain_evm_v1alpha_client_proto_init() } +func file_capabilities_blockchain_evm_v1alpha_client_proto_init() { + if File_capabilities_blockchain_evm_v1alpha_client_proto != nil { + return + } + file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[23].OneofWrappers = []any{} + file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes[25].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_capabilities_blockchain_evm_v1alpha_client_proto_rawDesc), len(file_capabilities_blockchain_evm_v1alpha_client_proto_rawDesc)), + NumEnums: 3, + NumMessages: 26, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_capabilities_blockchain_evm_v1alpha_client_proto_goTypes, + DependencyIndexes: file_capabilities_blockchain_evm_v1alpha_client_proto_depIdxs, + EnumInfos: file_capabilities_blockchain_evm_v1alpha_client_proto_enumTypes, + MessageInfos: file_capabilities_blockchain_evm_v1alpha_client_proto_msgTypes, + }.Build() + File_capabilities_blockchain_evm_v1alpha_client_proto = out.File + file_capabilities_blockchain_evm_v1alpha_client_proto_goTypes = nil + file_capabilities_blockchain_evm_v1alpha_client_proto_depIdxs = nil +} diff --git a/chain_capabilities/evm/protos/client.proto b/chain_capabilities/evm/protos/client.proto new file mode 100644 index 000000000..abd128746 --- /dev/null +++ b/chain_capabilities/evm/protos/client.proto @@ -0,0 +1,443 @@ +syntax = "proto3"; + +package capabilities.blockchain.evm.v1alpha; + +import "sdk/v1alpha/sdk.proto"; +import "tools/generator/v1alpha/cre_metadata.proto"; +import "values/v1/values.proto"; + +enum ConfidenceLevel { + CONFIDENCE_LEVEL_SAFE = 0; + CONFIDENCE_LEVEL_LATEST = 1; + CONFIDENCE_LEVEL_FINALIZED = 2; +} + +message TopicValues { + repeated bytes values = 1; // list of possible values for any topic, in [32]byte fix-sized array format +} + +message FilterLogTriggerRequest { + repeated bytes addresses = 1; // list of addresses to include in evm address [20]byte fix-sized array format, at least one address is required + /* + TopicValues is a fixed 4 length array of possible values for any topic where: + a) the first element is an array of the event signatures (keccak256 of the event name and indexed args types), it has to have at least one value + b) the second element is an array of possible values for the first indexed argument, can be empty + c) the third element is an array of possible values for the second indexed argument, can be empty + d) the fourth element is an array of possible values for the third indexed argument, can be empty + */ + repeated TopicValues topics = 2; + ConfidenceLevel confidence = 3; // optional, defaults to "SAFE" +} + +// CallContractRequest has arguments for reading a contract as specified in the call message at a block height defined by blockNumber where: +// blockNumber : +// nil (default) or (-2) → use the latest mined block (“latest”) +// FinalizedBlockNumber(-3) → last finalized block (“finalized”) +// +// Any positive value is treated as an explicit block height. +message CallContractRequest { + CallMsg call = 1; + values.v1.BigInt block_number = 2; +} + +message CallContractReply { + bytes data = 1; // solidity-spec abi encoded bytes +} + +message FilterLogsRequest { + FilterQuery filter_query = 1; +} + +message FilterLogsReply { + repeated Log logs = 1; +} + +// represents evm-style log +message Log { + bytes address = 1; // address of the contract emitted the log in evm address [20]byte fix-sized array format + repeated bytes topics = 2; // indexed log fields, in [32]byte fix-sized array format + bytes tx_hash = 3; // hash of the transaction containing the log, in [32]byte fix-sized array format + bytes block_hash = 4; // hash of the block containing the log, in [32]byte fix-sized array format + bytes data = 5; // solidity-spec abi encoded log Data + bytes event_sig = 6; // keccak256 of event signature, in [32]byte fix-sized array format + values.v1.BigInt block_number = 7; // block number containing the log + uint32 tx_index = 8; // index of transaction emmited the log + uint32 index = 9; // index of the Log within the intire block + bool removed = 10; // flag if the log was removed during reorg +} + +// represents simplified evm-style CallMsg +message CallMsg { + bytes from = 1; // sender address in evm address [20]byte fix-sized array format + bytes to = 2; // contract address in evm address [20]byte fix-sized array format + bytes data = 3; // solidity-spec abi encoded bytes +} + +// represents evm-style filter query +message FilterQuery { + bytes block_hash = 1; // exact block (cant use from/to), in [32]byte fix-sized array format + values.v1.BigInt from_block = 2; // start block range + values.v1.BigInt to_block = 3; // end block range + repeated bytes addresses = 4; // contract(s) to filter logs from in evm address [20]byte fix-sized array format + repeated Topics topics = 5; // filter log by event signature and indexed args +} + +message Topics { + repeated bytes topic = 1; // in [32]byte fix-sized array format +} + +message BalanceAtRequest { + bytes account = 1; // in evm address [20]byte fix-sized array format + values.v1.BigInt block_number = 2; +} + +message BalanceAtReply { + values.v1.BigInt balance = 1; // Balance of the account in wei (10^-18 eth) +} + +message EstimateGasRequest { + CallMsg msg = 1; // simulates tx execution returns approximate amount of gas units needed +} + +message EstimateGasReply { + uint64 gas = 1; // estimated amount of gas in gas units, needed for tx execution +} + +message GetTransactionByHashRequest { + bytes hash = 1; // in [32]byte fix-sized array format +} + +message GetTransactionByHashReply { + Transaction transaction = 1; +} + +// represents evm-style transaction +message Transaction { + uint64 nonce = 1; // number of txs sent from sender + uint64 gas = 2; // max gas allowed per execution (in gas units) + bytes to = 3; // recipient address in evm address [20]byte fix-sized array format + bytes data = 4; // solidity-spec abi encoded input data for function call payload + bytes hash = 5; // transaction hash, in [32]byte fix-sized array format + values.v1.BigInt value = 6; // amount of eth sent in wei + values.v1.BigInt gas_price = 7; // price for a single gas unit in wei +} + +message GetTransactionReceiptRequest { + bytes hash = 1; // in [32]byte fix-sized array format +} + +message GetTransactionReceiptReply { + Receipt receipt = 1; +} + +// represents evm-style receipt +message Receipt { + uint64 status = 1; // 1 for success 0 for failure + uint64 gas_used = 2; // gas used by this transaction (in gas units) + uint64 tx_index = 3; // index of the transaction inside of the block + bytes block_hash = 4; // block hash containing the transaction + repeated Log logs = 6; // logs emitted by this transaction + bytes tx_hash = 7; // hash of the transaction this receipt is for, in [32]byte fix-sized array format + values.v1.BigInt effective_gas_price = 8; // actual gas price paid in wei (include after EIP-1559) + values.v1.BigInt block_number = 9; // block number containing the transaction + bytes contract_address = 10; // address of the contract if this transaction created one in evm address [20]byte fix-sized array format +} + +// ----- Request/Reply Wrappers ----- +message HeaderByNumberRequest { + values.v1.BigInt block_number = 1; +} +message HeaderByNumberReply { + Header header = 1; +} + +message Header { + uint64 timestamp = 1; // unix timestamp + values.v1.BigInt block_number = 2; + bytes hash = 3; // in [32]byte fix-sized array format + bytes parent_hash = 4; // in [32]byte fix-sized array format +} + +service Client { + option (tools.generator.v1alpha.capability) = { + mode: MODE_DON + capability_id: "evm@1.0.0" + labels: { + // from https://github.com/smartcontractkit/chain-selectors/blob/main/selectors.yml + // as a subset of the selectors supported on the CRE + key: "ChainSelector" + value: { + uint64_label: { + defaults: [ + { + key: "adi-mainnet" + value: 4059281736450291836 + }, + { + key: "adi-testnet" + value: 9418205736192840573 + }, + { + key: "apechain-testnet-curtis" + value: 9900119385908781505 + }, + { + key: "arc-testnet" + value: 3034092155422581607 + }, + { + key: "avalanche-mainnet" + value: 6433500567565415381 + }, + { + key: "avalanche-testnet-fuji" + value: 14767482510784806043 + }, + { + key: "binance_smart_chain-mainnet" + value: 11344663589394136015 + }, + { + key: "binance_smart_chain-testnet" + value: 13264668187771770619 + }, + { + key: "celo-mainnet" + value: 1346049177634351622 + }, + { + key: "celo-sepolia" + value: 3761762704474186180 + }, + { + key: "cronos-testnet" + value: 2995292832068775165 + }, + { + key: "dtcc-mainnet-appchain" + value: 13879014182901017172 + }, + { + key: "dtcc-testnet-andesite" + value: 15513093881969820114 + }, + { + key: "ethereum-mainnet" + value: 5009297550715157269 + }, + { + key: "ethereum-mainnet-arbitrum-1" + value: 4949039107694359620 + }, + { + key: "ethereum-mainnet-base-1" + value: 15971525489660198786 + }, + { + key: "ethereum-mainnet-ink-1" + value: 3461204551265785888 + }, + { + key: "ethereum-mainnet-linea-1" + value: 4627098889531055414 + }, + { + key: "ethereum-mainnet-mantle-1" + value: 1556008542357238666 + }, + { + key: "ethereum-mainnet-optimism-1" + value: 3734403246176062136 + }, + { + key: "ethereum-mainnet-scroll-1" + value: 13204309965629103672 + }, + { + key: "ethereum-mainnet-worldchain-1" + value: 2049429975587534727 + }, + { + key: "ethereum-mainnet-xlayer-1" + value: 3016212468291539606 + }, + { + key: "ethereum-mainnet-zksync-1" + value: 1562403441176082196 + }, + { + key: "ethereum-testnet-sepolia" + value: 16015286601757825753 + }, + { + key: "ethereum-testnet-sepolia-arbitrum-1" + value: 3478487238524512106 + }, + { + key: "ethereum-testnet-sepolia-base-1" + value: 10344971235874465080 + }, + { + key: "ethereum-testnet-sepolia-linea-1" + value: 5719461335882077547 + }, + { + key: "ethereum-testnet-sepolia-mantle-1" + value: 8236463271206331221 + }, + { + key: "ethereum-testnet-sepolia-optimism-1" + value: 5224473277236331295 + }, + { + key: "ethereum-testnet-sepolia-scroll-1" + value: 2279865765895943307 + }, + { + key: "ethereum-testnet-sepolia-unichain-1" + value: 14135854469784514356 + }, + { + key: "ethereum-testnet-sepolia-worldchain-1" + value: 5299555114858065850 + }, + { + key: "ethereum-testnet-sepolia-zksync-1" + value: 6898391096552792247 + }, + { + key: "gnosis_chain-mainnet" + value: 465200170687744372 + }, + { + key: "gnosis_chain-testnet-chiado" + value: 8871595565390010547 + }, + { + key: "hyperliquid-mainnet" + value: 2442541497099098535 + }, + { + key: "hyperliquid-testnet" + value: 4286062357653186312 + }, + { + key: "ink-testnet-sepolia" + value: 9763904284804119144 + }, + { + key: "jovay-mainnet" + value: 1523760397290643893 + }, + { + key: "jovay-testnet" + value: 945045181441419236 + }, + { + key: "megaeth-mainnet" + value: 6093540873831549674 + }, + { + key: "megaeth-testnet-2" + value: 18241817625092392675 + }, + { + key: "pharos-atlantic-testnet" + value: 16098325658947243212 + }, + { + key: "pharos-mainnet" + value: 7801139999541420232 + }, + { + key: "plasma-mainnet" + value: 9335212494177455608 + }, + { + key: "plasma-testnet" + value: 3967220077692964309 + }, + { + key: "polygon-mainnet" + value: 4051577828743386545 + }, + { + key: "polygon-testnet-amoy" + value: 16281711391670634445 + }, + { + key: "private-testnet-andesite" + value: 6915682381028791124 + }, + { + key: "private-testnet-pumice" + value: 1564738277398880633 + }, + { + key: "private-testnet-quartzite" + value: 4175996748267305081 + }, + { + key: "private-testnet-rhyolite" + value: 604447335222770945 + }, + { + key: "sonic-mainnet" + value: 1673871237479749969 + }, + { + key: "sonic-testnet" + value: 1763698235108410440 + }, + { + key: "tac-testnet" + value: 9488606126177218005 + }, + { + key: "xlayer-testnet" + value: 10212741611335999305 + } + ] + } + } + } + }; + rpc CallContract(CallContractRequest) returns (CallContractReply); + rpc FilterLogs(FilterLogsRequest) returns (FilterLogsReply); + rpc BalanceAt(BalanceAtRequest) returns (BalanceAtReply); + rpc EstimateGas(EstimateGasRequest) returns (EstimateGasReply); + rpc GetTransactionByHash(GetTransactionByHashRequest) returns (GetTransactionByHashReply); + rpc GetTransactionReceipt(GetTransactionReceiptRequest) returns (GetTransactionReceiptReply); + rpc HeaderByNumber(HeaderByNumberRequest) returns (HeaderByNumberReply); + rpc LogTrigger(FilterLogTriggerRequest) returns (stream Log); + rpc WriteReport(WriteReportRequest) returns (WriteReportReply); +} + +enum ReceiverContractExecutionStatus { + RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS = 0; + RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED = 1; +} + +message WriteReportRequest { + bytes receiver = 1; + sdk.v1alpha.ReportResponse report = 2; + optional GasConfig gas_config = 3; +} + +message GasConfig { + uint64 gas_limit = 1; +} + +enum TxStatus { + TX_STATUS_FATAL = 0; + TX_STATUS_REVERTED = 1; + TX_STATUS_SUCCESS = 2; +} + +message WriteReportReply { + TxStatus tx_status = 1; + optional ReceiverContractExecutionStatus receiver_contract_execution_status = 2; + optional bytes tx_hash = 3; + optional values.v1.BigInt transaction_fee = 4; + optional string error_message = 5; +} diff --git a/chain_capabilities/evm/protos/client_server_gen.go b/chain_capabilities/evm/protos/client_server_gen.go new file mode 100644 index 000000000..0623518ec --- /dev/null +++ b/chain_capabilities/evm/protos/client_server_gen.go @@ -0,0 +1,271 @@ +// Code generated by github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/protoc, DO NOT EDIT. + +package protos + +import ( + "context" + "fmt" + "strconv" + + "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" +) + +// Avoid unused imports if there is configuration type +var _ = emptypb.Empty{} + +// ClientCapability is what a capability implements to be served as evm@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 ClientCapability interface { + CallContract(ctx context.Context, metadata capabilities.RequestMetadata, input *CallContractRequest) (*capabilities.ResponseAndMetadata[*CallContractReply], caperrors.Error) + + FilterLogs(ctx context.Context, metadata capabilities.RequestMetadata, input *FilterLogsRequest) (*capabilities.ResponseAndMetadata[*FilterLogsReply], caperrors.Error) + + BalanceAt(ctx context.Context, metadata capabilities.RequestMetadata, input *BalanceAtRequest) (*capabilities.ResponseAndMetadata[*BalanceAtReply], caperrors.Error) + + EstimateGas(ctx context.Context, metadata capabilities.RequestMetadata, input *EstimateGasRequest) (*capabilities.ResponseAndMetadata[*EstimateGasReply], caperrors.Error) + + GetTransactionByHash(ctx context.Context, metadata capabilities.RequestMetadata, input *GetTransactionByHashRequest) (*capabilities.ResponseAndMetadata[*GetTransactionByHashReply], caperrors.Error) + + GetTransactionReceipt(ctx context.Context, metadata capabilities.RequestMetadata, input *GetTransactionReceiptRequest) (*capabilities.ResponseAndMetadata[*GetTransactionReceiptReply], caperrors.Error) + + HeaderByNumber(ctx context.Context, metadata capabilities.RequestMetadata, input *HeaderByNumberRequest) (*capabilities.ResponseAndMetadata[*HeaderByNumberReply], caperrors.Error) + + RegisterLogTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *FilterLogTriggerRequest) (<-chan capabilities.TriggerAndId[*Log], caperrors.Error) + UnregisterLogTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *FilterLogTriggerRequest) caperrors.Error + + WriteReport(ctx context.Context, metadata capabilities.RequestMetadata, input *WriteReportRequest) (*capabilities.ResponseAndMetadata[*WriteReportReply], caperrors.Error) + AckEvent(ctx context.Context, triggerId string, eventId string, method string) caperrors.Error + + ChainSelector() uint64 + + Start(ctx context.Context) error + Close() error + HealthReport() map[string]error + Name() string + Description() string + Ready() error +} + +func NewClientServer(capability ClientCapability) *ClientServer { + stopCh := make(chan struct{}) + return &ClientServer{ + clientCapability: clientCapability{ClientCapability: capability, stopCh: stopCh}, + stopCh: stopCh, + } +} + +// ClientServer 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 ClientServer struct { + clientCapability + 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 *ClientServer) Close() error { + if c.stopCh != nil { + close(c.stopCh) + } + + return c.clientCapability.Close() +} + +type clientCapability struct { + ClientCapability + stopCh chan struct{} +} + +func (c *clientCapability) Info(ctx context.Context) (capabilities.CapabilityInfo, error) { + return capabilities.NewCapabilityInfo("evm"+":ChainSelector:"+strconv.FormatUint(c.ChainSelector(), 10)+"@1.0.0", capabilities.CapabilityTypeCombined, c.ClientCapability.Description()) +} + +var _ capabilities.ExecutableAndTriggerCapability = (*clientCapability)(nil) + +const ClientID = "evm@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 *clientCapability) Service() protoreflect.ServiceDescriptor { + return File_capabilities_blockchain_evm_v1alpha_client_proto.Services().ByName("Client") +} + +func (c *clientCapability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + case "LogTrigger": + input := &FilterLogTriggerRequest{} + return capabilities.RegisterTrigger(ctx, c.stopCh, "evm"+":ChainSelector:"+strconv.FormatUint(c.ChainSelector(), 10)+"@1.0.0", request, input, c.ClientCapability.RegisterLogTrigger) + default: + return nil, fmt.Errorf("trigger %s not found", request.Method) + } +} + +func (c *clientCapability) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error { + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + case "LogTrigger": + input := &FilterLogTriggerRequest{} + _, err := capabilities.FromValueOrAny(request.Config, request.Payload, input) + if err != nil { + return err + } + return c.ClientCapability.UnregisterLogTrigger(ctx, request.TriggerID, request.Metadata, input) + default: + return fmt.Errorf("method %s not found", request.Method) + } +} + +func (c *clientCapability) AckEvent(ctx context.Context, triggerId string, eventId string, method string) error { + switch method { + case "LogTrigger": + return c.ClientCapability.AckEvent(ctx, triggerId, eventId, method) + default: + return fmt.Errorf("trigger %s not found", method) + } +} + +func (c *clientCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (c *clientCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} + +func (c *clientCapability) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + response := capabilities.CapabilityResponse{} + ctx = request.Metadata.ContextWithCRE(ctx) + switch request.Method { + case "CallContract": + input := &CallContractRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *CallContractRequest, _ *emptypb.Empty) (*CallContractReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.CallContract(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 CallContract(..) (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 "FilterLogs": + input := &FilterLogsRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *FilterLogsRequest, _ *emptypb.Empty) (*FilterLogsReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.FilterLogs(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 FilterLogs(..) (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 "BalanceAt": + input := &BalanceAtRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *BalanceAtRequest, _ *emptypb.Empty) (*BalanceAtReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.BalanceAt(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 BalanceAt(..) (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 "EstimateGas": + input := &EstimateGasRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *EstimateGasRequest, _ *emptypb.Empty) (*EstimateGasReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.EstimateGas(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 EstimateGas(..) (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 "GetTransactionByHash": + input := &GetTransactionByHashRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *GetTransactionByHashRequest, _ *emptypb.Empty) (*GetTransactionByHashReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.GetTransactionByHash(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 GetTransactionByHash(..) (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 "GetTransactionReceipt": + input := &GetTransactionReceiptRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *GetTransactionReceiptRequest, _ *emptypb.Empty) (*GetTransactionReceiptReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.GetTransactionReceipt(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 GetTransactionReceipt(..) (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 "HeaderByNumber": + input := &HeaderByNumberRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *HeaderByNumberRequest, _ *emptypb.Empty) (*HeaderByNumberReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.HeaderByNumber(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 HeaderByNumber(..) (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 "WriteReport": + input := &WriteReportRequest{} + config := &emptypb.Empty{} + wrapped := func(ctx context.Context, metadata capabilities.RequestMetadata, input *WriteReportRequest, _ *emptypb.Empty) (*WriteReportReply, capabilities.ResponseMetadata, *capabilities.OCRAttestation, error) { + output, err := c.ClientCapability.WriteReport(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 WriteReport(..) (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/chain_capabilities/evm/protos/gen/main.go b/chain_capabilities/evm/protos/gen/main.go new file mode 100644 index 000000000..879455b81 --- /dev/null +++ b/chain_capabilities/evm/protos/gen/main.go @@ -0,0 +1,25 @@ +// Command gen generates the EVM 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/chain_capabilities/evm/protos/proto_helpers.go b/chain_capabilities/evm/protos/proto_helpers.go new file mode 100644 index 000000000..1ae277d26 --- /dev/null +++ b/chain_capabilities/evm/protos/proto_helpers.go @@ -0,0 +1,353 @@ +// Conversions between this capability's protos and the EVM types chainlink-common +// describes a chain with. Copied from chainlink-common's own copy of these protos +// (pkg/capabilities/v2/chain-capabilities/evm), which is generated from the same +// .proto this capability now generates for itself. + +package protos + +import ( + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/chains/evm" + evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" + valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" +) + +func ConvertHeaderToProto(h *evmtypes.Header) (*Header, error) { + if h == nil { + return nil, evm.ErrEmptyHead + } + + return &Header{ + Timestamp: h.Timestamp, + BlockNumber: valuespb.NewBigIntFromInt(h.Number), + Hash: h.Hash[:], + ParentHash: h.ParentHash[:], + }, nil +} + +func ConvertHeaderFromProto(protoHeader *Header) (evmtypes.Header, error) { + if protoHeader == nil { + return evmtypes.Header{}, evm.ErrEmptyHead + } + + hash, err := evm.ConvertHashFromProto(protoHeader.GetHash()) + if err != nil { + return evmtypes.Header{}, fmt.Errorf("failed to convert hash: %w", err) + } + + parentHash, err := evm.ConvertHashFromProto(protoHeader.GetParentHash()) + if err != nil { + return evmtypes.Header{}, fmt.Errorf("failed to convert parent hash: %w", err) + } + + return evmtypes.Header{ + Timestamp: protoHeader.GetTimestamp(), + Hash: hash, + ParentHash: parentHash, + Number: valuespb.NewIntFromBigInt(protoHeader.GetBlockNumber()), + }, nil +} + +func ConvertReceiptToProto(receipt *evmtypes.Receipt) (*Receipt, error) { + if receipt == nil { + return nil, evm.ErrEmptyReceipt + } + + logs, err := ConvertLogsToProto(receipt.Logs) + if err != nil { + return nil, fmt.Errorf("failed to convert logs: %w", err) + } + + return &Receipt{ + Status: receipt.Status, + Logs: logs, + TxHash: receipt.TxHash[:], + ContractAddress: receipt.ContractAddress[:], + GasUsed: receipt.GasUsed, + BlockHash: receipt.BlockHash[:], + BlockNumber: valuespb.NewBigIntFromInt(receipt.BlockNumber), + TxIndex: receipt.TransactionIndex, + EffectiveGasPrice: valuespb.NewBigIntFromInt(receipt.EffectiveGasPrice), + }, nil +} + +func ConvertReceiptFromProto(protoReceipt *Receipt) (*evmtypes.Receipt, error) { + if protoReceipt == nil { + return nil, evm.ErrEmptyReceipt + } + + logs, err := ConvertLogsFromProto(protoReceipt.GetLogs()) + if err != nil { + return nil, err + } + + txHash, err := evm.ConvertHashFromProto(protoReceipt.GetTxHash()) + if err != nil { + return nil, fmt.Errorf("failed to convert tx hash: %w", err) + } + + // can be empty on contract creation + contractAddress, err := evm.ConvertOptionalAddressFromProto(protoReceipt.GetContractAddress()) + if err != nil { + return nil, fmt.Errorf("failed to convert contract address: %w", err) + } + + blockHash, err := evm.ConvertHashFromProto(protoReceipt.GetBlockHash()) + if err != nil { + return nil, fmt.Errorf("failed to convert block hash: %w", err) + } + + return &evmtypes.Receipt{ + Status: protoReceipt.GetStatus(), + Logs: logs, + TxHash: txHash, + ContractAddress: contractAddress, + GasUsed: protoReceipt.GetGasUsed(), + BlockHash: blockHash, + BlockNumber: valuespb.NewIntFromBigInt(protoReceipt.GetBlockNumber()), + TransactionIndex: protoReceipt.GetTxIndex(), + EffectiveGasPrice: valuespb.NewIntFromBigInt(protoReceipt.GetEffectiveGasPrice()), + }, nil +} + +func ConvertTransactionToProto(tx *evmtypes.Transaction) (*Transaction, error) { + if tx == nil { + return nil, evm.ErrEmptyTx + } + + return &Transaction{ + To: tx.To[:], + Data: tx.Data, + Hash: tx.Hash[:], + Nonce: tx.Nonce, + Gas: tx.Gas, + GasPrice: valuespb.NewBigIntFromInt(tx.GasPrice), + Value: valuespb.NewBigIntFromInt(tx.Value), + }, nil +} + +func ConvertTransactionFromProto(protoTx *Transaction) (*evmtypes.Transaction, error) { + if protoTx == nil { + return nil, evm.ErrEmptyTx + } + + toAddress, err := evm.ConvertOptionalAddressFromProto(protoTx.GetTo()) + if err != nil { + return nil, fmt.Errorf("failed to convert 'to' address: %w", err) + } + + txHash, err := evm.ConvertHashFromProto(protoTx.GetHash()) + if err != nil { + return nil, fmt.Errorf("failed to convert tx hash: %w", err) + } + + return &evmtypes.Transaction{ + To: toAddress, + Data: protoTx.GetData(), + Hash: txHash, + Nonce: protoTx.GetNonce(), + Gas: protoTx.GetGas(), + GasPrice: valuespb.NewIntFromBigInt(protoTx.GetGasPrice()), + Value: valuespb.NewIntFromBigInt(protoTx.GetValue()), + }, nil +} + +func ConvertCallMsgToProto(msg *evmtypes.CallMsg) (*CallMsg, error) { + if msg == nil { + return nil, evm.ErrEmptyMsg + } + + return &CallMsg{ + From: msg.From[:], + To: msg.To[:], + Data: msg.Data, + }, nil +} + +func ConvertCallMsgFromProto(protoMsg *CallMsg) (*evmtypes.CallMsg, error) { + if protoMsg == nil { + return nil, evm.ErrEmptyMsg + } + + toAddress, err := evm.ConvertOptionalAddressFromProto(protoMsg.GetTo()) + if err != nil { + return nil, fmt.Errorf("failed to convert 'to' address: %w", err) + } + + callMsg := &evmtypes.CallMsg{ + Data: protoMsg.GetData(), + To: toAddress, + } + + // fromAddress is optional + if evm.ValidateAddressBytes(protoMsg.GetFrom()) == nil { + callMsg.From, err = evm.ConvertAddressFromProto(protoMsg.GetFrom()) + if err != nil { + return nil, fmt.Errorf("failed to convert 'from' address: %w", err) + } + } + + return callMsg, nil +} + +func ConvertFilterToProto(filter evmtypes.FilterQuery) (*FilterQuery, error) { + topics, err := convertTopicsToProto(filter.Topics) + if err != nil { + return nil, fmt.Errorf("%w: %w", evm.ErrTopicsConversion, err) + } + + return &FilterQuery{ + BlockHash: filter.BlockHash[:], + FromBlock: valuespb.NewBigIntFromInt(filter.FromBlock), + ToBlock: valuespb.NewBigIntFromInt(filter.ToBlock), + Addresses: evm.ConvertAddressesToProto(filter.Addresses), + Topics: topics, + }, nil +} + +func ConvertLogsToProto(logs []*evmtypes.Log) ([]*Log, error) { + protoLogs := make([]*Log, 0, len(logs)) + for i, log := range logs { + if log == nil { + return nil, fmt.Errorf("log[%d] can't be nil", i) + } + protoLogs = append(protoLogs, ConvertLogToProto(*log)) + } + return protoLogs, nil +} + +func ConvertFilterFromProto(protoFilter *FilterQuery) (evmtypes.FilterQuery, error) { + if protoFilter == nil { + return evmtypes.FilterQuery{}, evm.ErrEmptyFilter + } + + blockHash, err := evm.ConvertOptionalHashFromProto(protoFilter.GetBlockHash()) + if err != nil { + return evmtypes.FilterQuery{}, fmt.Errorf("failed to convert blockHash: %w", err) + } + + addresses, err := evm.ConvertAddressesFromProto(protoFilter.GetAddresses()) + if err != nil { + return evmtypes.FilterQuery{}, fmt.Errorf("failed to convert addresses: %w", err) + } + + topics, err := ConvertTopicsFromProto(protoFilter.GetTopics()) + if err != nil { + return evmtypes.FilterQuery{}, fmt.Errorf("%w: %w", evm.ErrTopicsConversion, err) + } + + return evmtypes.FilterQuery{ + BlockHash: blockHash, + FromBlock: valuespb.NewIntFromBigInt(protoFilter.GetFromBlock()), + ToBlock: valuespb.NewIntFromBigInt(protoFilter.GetToBlock()), + Addresses: addresses, + Topics: topics, + }, nil +} + +func ConvertLogsFromProto(protoLogs []*Log) ([]*evmtypes.Log, error) { + logs := make([]*evmtypes.Log, 0, len(protoLogs)) + for i, protoLog := range protoLogs { + if protoLog == nil { + return nil, fmt.Errorf("log at index %d can't be nil", i) + } + + l, err := convertLogFromProto(protoLog) + if err != nil { + return nil, fmt.Errorf("failed to convert log at index %d: %w", i, err) + } + logs = append(logs, l) + } + return logs, nil +} + +func ConvertTopicsFromProto(protoTopics []*Topics) ([][]evmtypes.Hash, error) { + topics := make([][]evmtypes.Hash, 0, len(protoTopics)) + for i, protoTopic := range protoTopics { + if protoTopic == nil { + return nil, fmt.Errorf("topic[%d] can't be nil", i) + } + + hashes, err := evm.ConvertHashesFromProto(protoTopic.GetTopic()) + if err != nil { + return nil, fmt.Errorf("failed to convert topics[%d]: %w", i, err) + } + + topics = append(topics, hashes) + } + return topics, nil +} + +func ConvertLogToProto(log evmtypes.Log) *Log { + return &Log{ + Index: log.LogIndex, + BlockHash: log.BlockHash[:], + BlockNumber: valuespb.NewBigIntFromInt(log.BlockNumber), + Topics: evm.ConvertHashesToProto(log.Topics), + EventSig: log.EventSig[:], + Address: log.Address[:], + TxHash: log.TxHash[:], + Data: log.Data[:], + // TODO tx index + //TxIndex: log.TxIndex + Removed: log.Removed, + } +} + +func convertTopicsToProto(topics [][]evmtypes.Hash) ([]*Topics, error) { + protoTopics := make([]*Topics, 0, len(topics)) + for i, topic := range topics { + if topic == nil { + return nil, fmt.Errorf("topic[%d] can't be nil", i) + } + + protoTopics = append(protoTopics, &Topics{Topic: evm.ConvertHashesToProto(topic)}) + } + return protoTopics, nil +} + +func convertLogFromProto(protoLog *Log) (*evmtypes.Log, error) { + if protoLog == nil { + return nil, errors.New("log can't be nil") + } + + blockHash, err := evm.ConvertHashFromProto(protoLog.GetBlockHash()) + if err != nil { + return nil, fmt.Errorf("failed to convert block hash: %w", err) + } + + topics, err := evm.ConvertHashesFromProto(protoLog.GetTopics()) + if err != nil { + return nil, fmt.Errorf("%w: %w", evm.ErrTopicsConversion, err) + } + + eventSigs, err := evm.ConvertHashFromProto(protoLog.GetEventSig()) + if err != nil { + return nil, fmt.Errorf("failed to convert event sig: %w", err) + } + + address, err := evm.ConvertAddressFromProto(protoLog.GetAddress()) + if err != nil { + return nil, err + } + + txHash, err := evm.ConvertHashFromProto(protoLog.GetTxHash()) + if err != nil { + return nil, fmt.Errorf("failed to convert tx hash: %w", err) + } + + return &evmtypes.Log{ + LogIndex: protoLog.GetIndex(), + BlockHash: blockHash, + BlockNumber: valuespb.NewIntFromBigInt(protoLog.GetBlockNumber()), + Topics: topics, + EventSig: eventSigs, + Address: address, + TxHash: txHash, + Data: protoLog.GetData(), + Removed: protoLog.GetRemoved(), + // TODO TxIndex + }, nil +} diff --git a/chain_capabilities/evm/protos/proto_helpers_test.go b/chain_capabilities/evm/protos/proto_helpers_test.go new file mode 100644 index 000000000..1a1352893 --- /dev/null +++ b/chain_capabilities/evm/protos/proto_helpers_test.go @@ -0,0 +1,420 @@ +package protos_test + +import ( + "testing" + + chainevm "github.com/smartcontractkit/chainlink-common/pkg/chains/evm" + evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" + valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + + protoevm "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func h32(b byte) evmtypes.Hash { + var h [32]byte + for i := range 32 { + h[i] = b + byte(i) + } + return h +} +func a20(b byte) evmtypes.Address { + var a [20]byte + for i := range 20 { + a[i] = b + byte(i) + } + return a +} +func b32(b byte) []byte { + out := make([]byte, 32) + for i := range 32 { + out[i] = b + byte(i) + } + return out +} +func b20(b byte) []byte { + out := make([]byte, 20) + for i := range 20 { + out[i] = b + byte(i) + } + return out +} +func zero20() []byte { return make([]byte, 20) } + +// Compares two *pb.BigInt by numeric value, ignoring internal Sign normalization. +func assertBigIntProtoEqual(t *testing.T, a, b *valuespb.BigInt) { + t.Helper() + assert.Equal(t, valuespb.NewIntFromBigInt(a), valuespb.NewIntFromBigInt(b)) +} + +func TestHeader_Conversions(t *testing.T) { + t.Run("nil guards", func(t *testing.T) { + _, err := protoevm.ConvertHeaderToProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyHead) + _, err = protoevm.ConvertHeaderFromProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyHead) + }) + + t.Run("roundtrip to proto", func(t *testing.T) { + h := &evmtypes.Header{ + Timestamp: 123456, + Hash: h32(0x10), + ParentHash: h32(0x11), + Number: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x01}, Sign: 0}), + } + p, err := protoevm.ConvertHeaderToProto(h) + require.NoError(t, err) + back, err := protoevm.ConvertHeaderFromProto(p) + require.NoError(t, err) + + assert.Equal(t, h.Timestamp, back.Timestamp) + assert.Equal(t, h.Hash, back.Hash) + assert.Equal(t, h.ParentHash, back.ParentHash) + assert.Equal(t, h.Number, back.Number) + }) + + t.Run("roundtrip from proto", func(t *testing.T) { + num := &valuespb.BigInt{AbsVal: []byte{0x02}, Sign: 0} + p := &protoevm.Header{ + Timestamp: 42, + BlockNumber: num, + Hash: b32(0x20), + ParentHash: b32(0x21), + } + d, err := protoevm.ConvertHeaderFromProto(p) + require.NoError(t, err) + p2, err := protoevm.ConvertHeaderToProto(&d) + require.NoError(t, err) + + assert.Equal(t, p.Timestamp, p2.Timestamp) + assert.Equal(t, p.Hash, p2.Hash) + assert.Equal(t, p.ParentHash, p2.ParentHash) + assertBigIntProtoEqual(t, p.BlockNumber, p2.BlockNumber) + }) +} + +func TestTransaction_Conversions(t *testing.T) { + t.Run("nil guards", func(t *testing.T) { + _, err := protoevm.ConvertTransactionToProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyTx) + _, err = protoevm.ConvertTransactionFromProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyTx) + }) + + t.Run("roundtrip to proto", func(t *testing.T) { + tx := &evmtypes.Transaction{ + To: a20(0x50), + Data: []byte{0xDE, 0xAD}, + Hash: h32(0x51), + Nonce: 7, + Gas: 50000, + GasPrice: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x0F}, Sign: 1}), + Value: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x0E}, Sign: 1}), + } + p, err := protoevm.ConvertTransactionToProto(tx) + require.NoError(t, err) + back, err := protoevm.ConvertTransactionFromProto(p) + require.NoError(t, err) + + assert.Equal(t, tx.To, back.To) + assert.Equal(t, tx.Data, back.Data) + assert.Equal(t, tx.Hash, back.Hash) + assert.Equal(t, tx.Nonce, back.Nonce) + assert.Equal(t, tx.Gas, back.Gas) + assert.Equal(t, tx.GasPrice, back.GasPrice) + assert.Equal(t, tx.Value, back.Value) + }) + + t.Run("roundtrip from proto (nil To, nil Data allowed)", func(t *testing.T) { + gp := &valuespb.BigInt{AbsVal: []byte{0x05}, Sign: 1} + val := &valuespb.BigInt{AbsVal: []byte{0x06}, Sign: 1} + p := &protoevm.Transaction{ + To: nil, // optional + Data: nil, // allowed + Hash: b32(0x61), + Nonce: 1, + Gas: 30000, + GasPrice: gp, + Value: val, + } + d, err := protoevm.ConvertTransactionFromProto(p) + require.NoError(t, err) + require.Nil(t, d.Data) // ensure nil stays nil + p2, err := protoevm.ConvertTransactionToProto(d) + require.NoError(t, err) + + assert.Equal(t, p.Hash, p2.Hash) + assert.Equal(t, p.Nonce, p2.Nonce) + assert.Equal(t, p.Gas, p2.Gas) + assertBigIntProtoEqual(t, p.GasPrice, p2.GasPrice) + assertBigIntProtoEqual(t, p.Value, p2.Value) + // To should be zero-address bytes after roundtrip + require.Len(t, p2.To, 20) + assert.Equal(t, zero20(), p2.To) + }) +} + +func TestCallMsg_Conversions(t *testing.T) { + t.Run("nil guards", func(t *testing.T) { + _, err := protoevm.ConvertCallMsgToProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyMsg) + _, err = protoevm.ConvertCallMsgFromProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyMsg) + }) + + t.Run("roundtrip to proto", func(t *testing.T) { + msg := &evmtypes.CallMsg{From: a20(0x01), To: a20(0x02), Data: []byte{1, 2, 3}} + p, err := protoevm.ConvertCallMsgToProto(msg) + require.NoError(t, err) + back, err := protoevm.ConvertCallMsgFromProto(p) + require.NoError(t, err) + + assert.Equal(t, msg.From, back.From) + assert.Equal(t, msg.To, back.To) + assert.Equal(t, msg.Data, back.Data) + }) + + t.Run("roundtrip from proto (optional To nil)", func(t *testing.T) { + p := &protoevm.CallMsg{From: b20(0x01), To: nil, Data: []byte{0xAA}} + d, err := protoevm.ConvertCallMsgFromProto(p) + require.NoError(t, err) + p2, err := protoevm.ConvertCallMsgToProto(d) + require.NoError(t, err) + + assert.Equal(t, p.From, p2.From) + assert.Equal(t, p.Data, p2.Data) + // To should become zero-address bytes after roundtrip + require.Len(t, p2.To, 20) + assert.Equal(t, zero20(), p2.To) + }) +} + +func TestReceipt_Conversions(t *testing.T) { + t.Run("nil guards", func(t *testing.T) { + _, err := protoevm.ConvertReceiptToProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyReceipt) + _, err = protoevm.ConvertReceiptFromProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyReceipt) + }) + + t.Run("roundtrip to proto", func(t *testing.T) { + r := &evmtypes.Receipt{ + Status: 1, + Logs: []*evmtypes.Log{{ + LogIndex: 7, + BlockHash: h32(0xA0), + BlockNumber: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x09}, Sign: 0}), + Topics: []evmtypes.Hash{h32(0xA1), h32(0xA2)}, + EventSig: h32(0xA3), + Address: a20(0xA4), + TxHash: h32(0xA5), + Data: []byte{0xDE, 0xAD, 0xBE, 0xEF}, + Removed: true, + }}, + TxHash: h32(0x30), + ContractAddress: a20(0x31), + GasUsed: 21000, + BlockHash: h32(0x32), + BlockNumber: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x02}, Sign: 0}), + TransactionIndex: 9, + EffectiveGasPrice: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x03}, Sign: 0}), + } + p, err := protoevm.ConvertReceiptToProto(r) + require.NoError(t, err) + back, err := protoevm.ConvertReceiptFromProto(p) + require.NoError(t, err) + + assert.Equal(t, r.Status, back.Status) + assert.Equal(t, r.TxHash, back.TxHash) + assert.Equal(t, r.ContractAddress, back.ContractAddress) + assert.Equal(t, r.GasUsed, back.GasUsed) + assert.Equal(t, r.BlockHash, back.BlockHash) + assert.Equal(t, r.BlockNumber, back.BlockNumber) + assert.Equal(t, r.TransactionIndex, back.TransactionIndex) + assert.Equal(t, r.EffectiveGasPrice, back.EffectiveGasPrice) + + require.Len(t, back.Logs, 1) + gl := back.Logs[0] + ol := r.Logs[0] + assert.Equal(t, ol.LogIndex, gl.LogIndex) + assert.Equal(t, ol.BlockHash, gl.BlockHash) + assert.Equal(t, ol.BlockNumber, gl.BlockNumber) + assert.Equal(t, ol.Topics, gl.Topics) + assert.Equal(t, ol.EventSig, gl.EventSig) + assert.Equal(t, ol.Address, gl.Address) + assert.Equal(t, ol.TxHash, gl.TxHash) + assert.Equal(t, ol.Data, gl.Data) + assert.Equal(t, ol.Removed, gl.Removed) + }) + + t.Run("roundtrip from proto", func(t *testing.T) { + num := &valuespb.BigInt{AbsVal: []byte{0x01}, Sign: 0} + price := &valuespb.BigInt{AbsVal: []byte{0x02}, Sign: 0} + plog := &protoevm.Log{ + Index: 5, + BlockHash: b32(0xB0), + BlockNumber: &valuespb.BigInt{AbsVal: []byte{0x04}, Sign: 0}, + Topics: [][]byte{b32(0xB1), b32(0xB2)}, + EventSig: b32(0xB3), + Address: b20(0xB4), + TxHash: b32(0xB5), + Data: []byte{0xBE, 0xEF}, + Removed: false, + } + p := &protoevm.Receipt{ + Status: 1, + Logs: []*protoevm.Log{plog}, + TxHash: b32(0x40), + ContractAddress: b20(0x41), + GasUsed: 21000, + BlockHash: b32(0x42), + BlockNumber: num, + TxIndex: 3, + EffectiveGasPrice: price, + } + d, err := protoevm.ConvertReceiptFromProto(p) + require.NoError(t, err) + p2, err := protoevm.ConvertReceiptToProto(d) + require.NoError(t, err) + + assert.Equal(t, p.Status, p2.Status) + assert.Equal(t, p.TxHash, p2.TxHash) + assert.Equal(t, p.ContractAddress, p2.ContractAddress) + assert.Equal(t, p.GasUsed, p2.GasUsed) + assert.Equal(t, p.BlockHash, p2.BlockHash) + assertBigIntProtoEqual(t, p.BlockNumber, p2.BlockNumber) + assert.Equal(t, p.TxIndex, p2.TxIndex) + assertBigIntProtoEqual(t, p.EffectiveGasPrice, p2.EffectiveGasPrice) + + require.Len(t, p2.Logs, 1) + l2 := p2.Logs[0] + assert.Equal(t, plog.Index, l2.Index) + assert.Equal(t, plog.BlockHash, l2.BlockHash) + assertBigIntProtoEqual(t, plog.BlockNumber, l2.BlockNumber) + assert.Equal(t, plog.Topics, l2.Topics) + assert.Equal(t, plog.EventSig, l2.EventSig) + assert.Equal(t, plog.Address, l2.Address) + assert.Equal(t, plog.TxHash, l2.TxHash) + assert.Equal(t, plog.Data, l2.Data) + assert.Equal(t, plog.Removed, l2.Removed) + }) +} + +func TestFilterQuery_Conversions(t *testing.T) { + t.Run("nil guard FromProto", func(t *testing.T) { + _, err := protoevm.ConvertFilterFromProto(nil) + require.ErrorIs(t, err, chainevm.ErrEmptyFilter) + }) + + t.Run("roundtrip to proto", func(t *testing.T) { + d := evmtypes.FilterQuery{ + BlockHash: h32(0x90), + FromBlock: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x01}, Sign: 0}), + ToBlock: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x02}, Sign: 0}), + Addresses: []evmtypes.Address{a20(0x91), a20(0x92)}, + Topics: [][]evmtypes.Hash{ + {h32(0x93)}, + {}, // empty (non-nil) is allowed + }, + } + p, err := protoevm.ConvertFilterToProto(d) + require.NoError(t, err) + back, err := protoevm.ConvertFilterFromProto(p) + require.NoError(t, err) + + assert.Equal(t, d.BlockHash, back.BlockHash) + assert.Equal(t, d.FromBlock, back.FromBlock) + assert.Equal(t, d.ToBlock, back.ToBlock) + assert.Equal(t, d.Addresses, back.Addresses) + assert.Equal(t, d.Topics, back.Topics) + }) + + t.Run("roundtrip from proto", func(t *testing.T) { + p := &protoevm.FilterQuery{ + BlockHash: b32(0x9A), + FromBlock: &valuespb.BigInt{AbsVal: []byte{0x03}, Sign: 0}, + ToBlock: &valuespb.BigInt{AbsVal: []byte{0x04}, Sign: 0}, + Addresses: [][]byte{b20(0x9B), b20(0x9C)}, + Topics: []*protoevm.Topics{ + {Topic: [][]byte{b32(0x9D)}}, + {Topic: [][]byte{}}, // empty inner slice is OK + }, + } + d, err := protoevm.ConvertFilterFromProto(p) + require.NoError(t, err) + p2, err := protoevm.ConvertFilterToProto(d) + require.NoError(t, err) + + assert.Equal(t, p.BlockHash, p2.BlockHash) + assertBigIntProtoEqual(t, p.FromBlock, p2.FromBlock) + assertBigIntProtoEqual(t, p.ToBlock, p2.ToBlock) + assert.Equal(t, p.Addresses, p2.Addresses) + require.Len(t, p2.Topics, len(p.Topics)) + for i := range p.Topics { + assert.Equal(t, p.Topics[i].Topic, p2.Topics[i].Topic) + } + }) +} + +func TestLog_Conversions(t *testing.T) { + t.Run("roundtrip to proto", func(t *testing.T) { + l := evmtypes.Log{ + LogIndex: 7, + BlockHash: h32(0xA0), + BlockNumber: valuespb.NewIntFromBigInt(&valuespb.BigInt{AbsVal: []byte{0x09}, Sign: 0}), + Topics: []evmtypes.Hash{h32(0xA1), h32(0xA2)}, + EventSig: h32(0xA3), + Address: a20(0xA4), + TxHash: h32(0xA5), + Data: []byte{0xDE, 0xAD, 0xBE, 0xEF}, + Removed: true, + } + pl := protoevm.ConvertLogToProto(l) + backLogs, err := protoevm.ConvertLogsFromProto([]*protoevm.Log{pl}) + require.NoError(t, err) + require.Len(t, backLogs, 1) + got := backLogs[0] + + assert.Equal(t, l.LogIndex, got.LogIndex) + assert.Equal(t, l.BlockHash, got.BlockHash) + assert.Equal(t, l.BlockNumber, got.BlockNumber) + assert.Equal(t, l.Topics, got.Topics) + assert.Equal(t, l.EventSig, got.EventSig) + assert.Equal(t, l.Address, got.Address) + assert.Equal(t, l.TxHash, got.TxHash) + assert.Equal(t, l.Data, got.Data) + assert.Equal(t, l.Removed, got.Removed) + }) + + t.Run("roundtrip from proto", func(t *testing.T) { + pl := &protoevm.Log{ + Index: 5, + BlockHash: b32(0xB0), + BlockNumber: &valuespb.BigInt{AbsVal: []byte{0x04}, Sign: 0}, + Topics: [][]byte{b32(0xB1), b32(0xB2)}, + EventSig: b32(0xB3), + Address: b20(0xB4), + TxHash: b32(0xB5), + Data: []byte{0xBE, 0xEF}, + Removed: false, + } + dLogs, err := protoevm.ConvertLogsFromProto([]*protoevm.Log{pl}) + require.NoError(t, err) + pLogs, err := protoevm.ConvertLogsToProto(dLogs) + require.NoError(t, err) + require.Len(t, pLogs, 1) + + got := pLogs[0] + assert.Equal(t, pl.Index, got.Index) + assert.Equal(t, pl.BlockHash, got.BlockHash) + assertBigIntProtoEqual(t, pl.BlockNumber, got.BlockNumber) + assert.Equal(t, pl.Topics, got.Topics) + assert.Equal(t, pl.EventSig, got.EventSig) + assert.Equal(t, pl.Address, got.Address) + assert.Equal(t, pl.TxHash, got.TxHash) + assert.Equal(t, pl.Data, got.Data) + assert.Equal(t, pl.Removed, got.Removed) + }) +} diff --git a/chain_capabilities/evm/simulated/deployment.go b/chain_capabilities/evm/simulated/deployment.go new file mode 100644 index 000000000..98606690d --- /dev/null +++ b/chain_capabilities/evm/simulated/deployment.go @@ -0,0 +1,358 @@ +// Package simulated is what a local run needs put on the chain it was given, when +// that chain is one this process started for itself. +// +// The chain is chainlink-evm's (see its cre/evm.SimulatedChain): every consumer of +// that dependency gets one when it is pointed at nothing, so this is not about +// having a chain. It is about what a deployment would have done to it - fund the +// accounts the instances send from, deploy the forwarder they write reports +// through, and tell that forwarder who they are - which is CRE's business and not +// a chain library's. +// +// It does for an embedded run what the local CRE's deployment does around a node, +// so that "embed" with nothing configured is a DON that can actually write. +package simulated + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/balance_reader" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/forwarder" + capabilities_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + workflow_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/workflow_registry_wrapper_v2" + creevm "github.com/smartcontractkit/chainlink-evm/pkg/cre/evm" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// Deployment is what was put on the simulated chain for this run. +// +// It is what the local CRE deploys around a node for the EVM capability: the +// forwarder the environment itself puts on every chain a DON writes through, and +// the two contracts its EVM tests deploy beside it - a balance reader to read and +// a data feeds cache to write into. +// +// Every address here is the same on every run. They are created by one account +// whose key is derived from a constant, on a chain that starts empty, in the +// order below - so each is that account and a nonce, and nothing about a +// particular run (how many instances, whether they were funded first) moves them. +// See deploy, where that order is the point. +type Deployment struct { + // Forwarder is the deployed CRE forwarder: what a report is transmitted through, + // configured with this run's instances as its DON so that it accepts what they + // sign and nothing else. + Forwarder common.Address + + // Configured says whether that last part happened. The contract requires a DON + // that can tolerate a fault - F of at least one, with 3F < N - so a run of fewer + // than four instances has a forwarder deployed and no DON on it: a chain to read, + // trigger and observe on, and too few members to write through. + Configured bool + + // CapabilitiesRegistry and WorkflowRegistry are what "env start" deploys on the + // registry chain before anything else (DeployV2RegistryContractsSequence): the + // first says which DONs exist and what they can do, the second which workflows are + // registered against them. + // + // They are deployed empty. Registering this run's DON, its nodes and its + // capabilities in them is configuration rather than deployment, and an embedded run + // does not need it: its capability registry is in process, which is what embedding + // means. They are here so that what is on this chain is what is on the local CRE's, + // and so that anything reading them finds a contract rather than an empty address. + CapabilitiesRegistry common.Address + WorkflowRegistry common.Address + + // BalanceReader is what the local CRE's example workflows read through - "env start + // --with-example" deploys one - so a workflow written against that has it here too. + BalanceReader common.Address + + // Accounts are the instances' own, funded in instance order. + Accounts []common.Address +} + +// DefaultReceiverGasMinimum is the gas a receiving contract is guaranteed on a +// simulated chain when nothing said otherwise, matching the local CRE's own +// default so that a workflow behaves here as it does there. +const DefaultReceiverGasMinimum = 500 + +// Config is what this run puts on the chain, as opposed to the chain itself. +type Config struct { + // DonID is the DON the forwarder is configured for, which has to be the DON the + // instances report as. It defaults to the same 1 --capabilities.capability-don-id + // does. + DonID uint32 `usage:"DON ID the simulated chain's forwarder is configured for; must match --capabilities.capability-don-id"` +} + +// Defaults are what a run that says nothing gets. +var Defaults = Config{DonID: 1} + +// Dependency returns what was deployed on the simulated chain, or nil when there +// is no simulated chain - which is every configured run, and any embedded run +// pointed at a real chain. +// +// keystores is how it learns which accounts to fund: the same function the +// instances themselves sign with, asked for each of them, so what is funded is +// what will send. +func Dependency( + lggr logger.Logger, + chain standalone.BootstrapDependency[*creevm.SimulatedChain], + keystores func(instance int) (core.Keystore, error), +) standalone.BootstrapDependency[*Deployment] { + return &dependency{lggr: lggr, chain: chain, keystores: keystores} +} + +type dependency struct { + lggr logger.Logger + chain standalone.BootstrapDependency[*creevm.SimulatedChain] + keystores func(instance int) (core.Keystore, error) + + // cfg and instances are the embedded form's; a configured run has neither, and + // nothing to deploy. + cfg *Config + instances int + + started bool + deployment *Deployment + err error + + // embedded is the one embedded form, since the bootstrapper asks for one per + // instance and once more to collect settings: they are one DON on one chain, and + // the settings read have to be the settings bound to flags. + embedded *dependency +} + +var _ standalone.BootstrapDependency[*Deployment] = (*dependency)(nil) + +func (d *dependency) Namespace() string { return "simulated" } + +func (d *dependency) Config() any { + if d.cfg == nil { + return nil + } + return d.cfg +} + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{d.chain} +} + +func (d *dependency) ForEmbedding(i, instances int) standalone.BootstrapDependency[*Deployment] { + if d.embedded == nil { + cfg := Defaults + d.embedded = &dependency{lggr: d.lggr, keystores: d.keystores, cfg: &cfg} + } + // The largest wins: this is called once per instance with the real count, and once + // before any of them as (0, 1) to collect the settings to register. What is funded + // and configured should be the run, not the probe. + d.embedded.instances = max(d.embedded.instances, instances) + d.embedded.chain = d.chain.ForEmbedding(i, instances) + return d.embedded +} + +// Get funds and deploys once, however many instances ask: they share a chain, and +// what goes on it goes on it once. +func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (*Deployment, error) { + chain, err := d.chain.Get(ctx, cc) + if err != nil { + return nil, err + } + if chain == nil || d.cfg == nil { + // A real chain: what is on it was put there by a deployment, and this has nothing + // to say about it. + return nil, nil + } + + if !d.started { + d.started = true + d.deployment, d.err = d.deploy(ctx, chain) + } + return d.deployment, d.err +} + +// deploy puts the contracts on the chain and then funds the accounts. +// +// That order is load-bearing. A contract's address is the deploying account and +// its nonce, so deploying first - before anything else this account sends - puts +// every contract at an address that depends on nothing but this list and the +// order of it. Funding first would move them with the instance count; configuring +// the forwarder in between would move them for a run too small to configure it. +// +// So: deploy everything, then configure, then fund. Adding a contract to the end +// of the list leaves the ones before it where they were. +func (d *dependency) deploy(ctx context.Context, chain *creevm.SimulatedChain) (*Deployment, error) { + deployment := &Deployment{} + var err error + + // In the order "env start" deploys them: the registries first, since everything + // else on a real environment is registered in them, then the forwarder a DON + // writes through, then what the examples read. + if deployment.CapabilitiesRegistry, err = deployCapabilitiesRegistry(ctx, chain); err != nil { + return nil, err + } + if deployment.WorkflowRegistry, err = deployWorkflowRegistry(ctx, chain); err != nil { + return nil, err + } + + forwarder, err := deployForwarder(ctx, chain) + if err != nil { + return nil, err + } + deployment.Forwarder = forwarder.address + + if deployment.BalanceReader, err = deployBalanceReader(ctx, chain); err != nil { + return nil, err + } + + signers, err := signers(d.instances) + if err != nil { + return nil, err + } + if deployment.Configured, err = configureForwarder(ctx, d.lggr, chain, forwarder, d.cfg.DonID, signers); err != nil { + return nil, err + } + + if deployment.Accounts, err = d.accounts(ctx); err != nil { + return nil, err + } + for _, account := range deployment.Accounts { + if err := chain.Fund(ctx, account, creevm.DefaultSimulatedConfig.FundingAmount()); err != nil { + return nil, err + } + } + + d.lggr.Infow("Deployed the contracts the local CRE sets up", + "capabilitiesRegistry", deployment.CapabilitiesRegistry, "workflowRegistry", deployment.WorkflowRegistry, + "forwarder", deployment.Forwarder, "forwarderHasDON", deployment.Configured, + "balanceReader", deployment.BalanceReader, "funded", deployment.Accounts) + + return deployment, nil +} + +// accounts are the instances' own, asked of the keystores they sign with. +func (d *dependency) accounts(ctx context.Context) ([]common.Address, error) { + accounts := make([]common.Address, 0, d.instances) + for instance := range d.instances { + keystore, err := d.keystores(instance) + if err != nil { + return nil, fmt.Errorf("failed to read the account of instance %d: %w", instance, err) + } + held, err := keystore.Accounts(ctx) + if err != nil { + return nil, fmt.Errorf("failed to read the account of instance %d: %w", instance, err) + } + if len(held) != 1 { + return nil, fmt.Errorf("instance %d holds %d accounts, want one to fund", instance, len(held)) + } + if !common.IsHexAddress(held[0]) { + return nil, fmt.Errorf("instance %d holds %q, which is not an address to fund", instance, held[0]) + } + accounts = append(accounts, common.HexToAddress(held[0])) + } + return accounts, nil +} + +// deployed is a contract on the chain: where it is, and the binding to call it. +type deployed struct { + address common.Address + contract *forwarder.KeystoneForwarder +} + +// deployForwarder deploys the CRE forwarder, which is what a report is +// transmitted through. The local CRE's environment puts one on every chain a DON +// writes to; this is that, for a chain that has no environment around it. +func deployForwarder(ctx context.Context, chain *creevm.SimulatedChain) (deployed, error) { + address, tx, contract, err := forwarder.DeployKeystoneForwarder(chain.Transactor(), chain.Backend()) + if err != nil { + return deployed{}, fmt.Errorf("failed to deploy the forwarder: %w", err) + } + if err := chain.Mined(ctx, tx); err != nil { + return deployed{}, fmt.Errorf("the forwarder was not deployed: %w", err) + } + return deployed{address: address, contract: contract}, nil +} + +// deployBalanceReader deploys the contract the local CRE's EVM read tests read +// through, so a workflow written against those has it here too. +func deployBalanceReader(ctx context.Context, chain *creevm.SimulatedChain) (common.Address, error) { + address, tx, _, err := balance_reader.DeployBalanceReader(chain.Transactor(), chain.Backend()) + if err != nil { + return common.Address{}, fmt.Errorf("failed to deploy the balance reader: %w", err) + } + if err := chain.Mined(ctx, tx); err != nil { + return common.Address{}, fmt.Errorf("the balance reader was not deployed: %w", err) + } + return address, nil +} + +// deployCapabilitiesRegistry deploys the registry that says which DONs exist and +// what they can do. See Deployment.CapabilitiesRegistry for why it is left empty. +// +// CanAddOneNodeDONs is what the local CRE's deployment passes: false. +func deployCapabilitiesRegistry(ctx context.Context, chain *creevm.SimulatedChain) (common.Address, error) { + address, tx, _, err := capabilities_registry_v2.DeployCapabilitiesRegistry( + chain.Transactor(), chain.Backend(), capabilities_registry_v2.CapabilitiesRegistryConstructorParams{}) + if err != nil { + return common.Address{}, fmt.Errorf("failed to deploy the capabilities registry: %w", err) + } + if err := chain.Mined(ctx, tx); err != nil { + return common.Address{}, fmt.Errorf("the capabilities registry was not deployed: %w", err) + } + return address, nil +} + +// deployWorkflowRegistry deploys the registry a workflow is registered in, the +// other half of what "env start" puts on the registry chain. +func deployWorkflowRegistry(ctx context.Context, chain *creevm.SimulatedChain) (common.Address, error) { + address, tx, _, err := workflow_registry_v2.DeployWorkflowRegistry(chain.Transactor(), chain.Backend()) + if err != nil { + return common.Address{}, fmt.Errorf("failed to deploy the workflow registry: %w", err) + } + if err := chain.Mined(ctx, tx); err != nil { + return common.Address{}, fmt.Errorf("the workflow registry was not deployed: %w", err) + } + return address, nil +} + +// configureForwarder tells the forwarder who this run's oracles are, which is +// what the local CRE's deployment does around a real DON: a report reaches a +// receiver only if the signatures on it are theirs. +// +// It reports whether that happened. The contract requires a DON that can tolerate +// a fault - F of at least one, with 3F < N - so a run of fewer than four +// instances is left with a forwarder and no DON on it, which is the honest state +// for a run that small: reading, triggering and observing all work without one. +func configureForwarder( + ctx context.Context, + lggr logger.Logger, + chain *creevm.SimulatedChain, + forwarder deployed, + donID uint32, + signers []common.Address, +) (bool, error) { + // F is the largest fault tolerance the oracle count allows, which is what the OCR + // configuration these same instances run under uses. + f := (len(signers) - 1) / 3 + if f < 1 { + lggr.Warnw("The simulated chain's forwarder has no DON, so reports cannot be written through it", + "instances", len(signers), "reason", "the forwarder requires F >= 1 with 3F < N, so at least four instances", + "forwarder", forwarder.address) + return false, nil + } + + tx, err := forwarder.contract.SetConfig(chain.Transactor(), donID, 1, uint8(f), signers) //#nosec G115 - an embedded run has a handful of instances + if err != nil { + return false, fmt.Errorf("failed to configure the forwarder: %w", err) + } + if err := chain.Mined(ctx, tx); err != nil { + return false, fmt.Errorf("the forwarder was not configured: %w", err) + } + + lggr.Infow("Configured the forwarder with this run's oracles", + "forwarder", forwarder.address, "donID", donID, "f", f, "signers", signers) + + return true, nil +} diff --git a/chain_capabilities/evm/simulated/deployment_test.go b/chain_capabilities/evm/simulated/deployment_test.go new file mode 100644 index 000000000..7c0c8a8e5 --- /dev/null +++ b/chain_capabilities/evm/simulated/deployment_test.go @@ -0,0 +1,183 @@ +package simulated + +import ( + "context" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/forwarder" + creevm "github.com/smartcontractkit/chainlink-evm/pkg/cre/evm" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/chain" +) + +// chainDependency hands back one already-started chain, which is what the client +// dependency does for an embedded run told of none. +type chainDependency struct{ chain *creevm.SimulatedChain } + +var _ standalone.BootstrapDependency[*creevm.SimulatedChain] = (*chainDependency)(nil) + +func (d *chainDependency) Namespace() string { return "" } +func (d *chainDependency) Config() any { return nil } +func (d *chainDependency) Dependencies() []standalone.BootstrapCommand { return nil } + +func (d *chainDependency) ForEmbedding(int, int) standalone.BootstrapDependency[*creevm.SimulatedChain] { + return d +} + +func (d *chainDependency) Get(context.Context, standalone.CommonConfig) (*creevm.SimulatedChain, error) { + return d.chain, nil +} + +func simulatedChain(t *testing.T) *creevm.SimulatedChain { + t.Helper() + + chain, err := creevm.StartSimulated(t.Context(), logger.Test(t), creevm.SimulatedConfig{BlockTime: 50 * time.Millisecond}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, chain.Close()) }) + return chain +} + +// keystores are the instances' own, derived the way an embedded run derives them. +func keystores(instance int) (core.Keystore, error) { return chain.DeterministicKeystore(instance) } + +// TestDeployment covers what a run of four gets: every instance's account funded, +// and a forwarder that knows them as its DON - which is what has to be true +// before a report written by this run can land. +func TestDeployment(t *testing.T) { + const instances = 4 + + sim := simulatedChain(t) + dep := Dependency(logger.Test(t), &chainDependency{chain: sim}, keystores).ForEmbedding(0, instances) + + deployment, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + require.NotNil(t, deployment) + + t.Run("the accounts the instances send from are funded", func(t *testing.T) { + require.Len(t, deployment.Accounts, instances) + + for i, account := range deployment.Accounts { + expected, err := chain.DeterministicAddress(i) + require.NoError(t, err) + assert.Equal(t, expected, account, "the account funded must be the one instance %d signs as", i) + + balance, err := sim.Backend().BalanceAt(t.Context(), account, nil) + require.NoError(t, err) + assert.Positive(t, balance.Sign(), "instance %d cannot send with an empty account", i) + } + }) + + t.Run("the forwarder knows this run as its DON", func(t *testing.T) { + require.True(t, deployment.Configured) + + contract, err := forwarder.NewKeystoneForwarderFilterer(deployment.Forwarder, sim.Backend()) + require.NoError(t, err) + + // Read back from what the contract emitted, since it exposes no getter for it. + configs, err := contract.FilterConfigSet(&bind.FilterOpts{Context: t.Context()}, []uint32{Defaults.DonID}, nil) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, configs.Close()) }) + + require.True(t, configs.Next(), "the forwarder was never told who this DON is") + + wanted, err := signers(instances) + require.NoError(t, err) + assert.Equal(t, wanted, configs.Event.Signers, "the DON must be the oracles this run signs with") + + // F is the largest the instance count allows: four members tolerate one fault, + // and a report needs F+1 of their signatures. + assert.Equal(t, uint8(1), configs.Event.F) + assert.False(t, configs.Next(), "and must be configured once") + }) + + t.Run("the other contracts a local CRE run has are there too", func(t *testing.T) { + for name, address := range map[string]common.Address{ + "capabilities registry": deployment.CapabilitiesRegistry, + "workflow registry": deployment.WorkflowRegistry, + "balance reader": deployment.BalanceReader, + } { + code, err := sim.Backend().CodeAt(t.Context(), address, nil) + require.NoError(t, err) + assert.NotEmpty(t, code, "%s is named but nothing was deployed there", name) + } + }) + + t.Run("asking again is the same deployment", func(t *testing.T) { + again, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + assert.Same(t, deployment, again, "the instances share a chain, and what is on it goes on once") + }) +} + +// TestDeploymentTooSmallToWrite covers the honest state of a run below four: a +// chain to read and trigger on, and a forwarder with no DON, because the contract +// will not have one that cannot tolerate a fault. +func TestDeploymentTooSmallToWrite(t *testing.T) { + sim := simulatedChain(t) + dep := Dependency(logger.Test(t), &chainDependency{chain: sim}, keystores).ForEmbedding(0, 1) + + deployment, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + require.NotNil(t, deployment) + + assert.False(t, deployment.Configured) + assert.NotEqual(t, common.Address{}, deployment.Forwarder, "it is still deployed, so a run can read it") + assert.Len(t, deployment.Accounts, 1, "and the one instance is still funded") +} + +// TestDeploymentWithoutASimulatedChain is a configured run, or an embedded one +// pointed at a real chain: what is deployed there was deployed by a deployment, +// and this has nothing to say about it. +func TestDeploymentWithoutASimulatedChain(t *testing.T) { + dep := Dependency(logger.Test(t), &chainDependency{}, keystores).ForEmbedding(0, 4) + + deployment, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + assert.Nil(t, deployment) +} + +// TestDeploymentAddressesAreFixed is the property a local run leans on: the +// addresses are the same every time, so a workflow, a test or a note in a +// terminal can name one before the chain it is on exists. +// +// They are the deploying account and a nonce, and that account is derived from a +// constant - so what has to hold is that nothing else this deployment does moves +// the nonces around. Funding is what would: it sends from the same account, and +// there is one transfer per instance. +func TestDeploymentAddressesAreFixed(t *testing.T) { + deploy := func(t *testing.T, instances int) *Deployment { + t.Helper() + + dep := Dependency(logger.Test(t), &chainDependency{chain: simulatedChain(t)}, keystores).ForEmbedding(0, instances) + deployment, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + require.NotNil(t, deployment) + return deployment + } + + // Two chains, and a different number of accounts funded on each: neither the run + // nor its size may decide where a contract lives. + four, one := deploy(t, 4), deploy(t, 1) + + assert.Equal(t, four.CapabilitiesRegistry, one.CapabilitiesRegistry) + assert.Equal(t, four.WorkflowRegistry, one.WorkflowRegistry) + assert.Equal(t, four.Forwarder, one.Forwarder) + assert.Equal(t, four.BalanceReader, one.BalanceReader) + + // Pinned, so that changing the order contracts are deployed in - which would move + // every address after the one that moved - is a change someone has to mean. + assert.Equal(t, "0x88aEF42dd4f3598beBE4c3e3cbE03e638f175330", four.CapabilitiesRegistry.Hex()) + assert.Equal(t, "0x380D3Dd74169897d48ad627F3ebc665ea158A0cE", four.WorkflowRegistry.Hex()) + assert.Equal(t, "0xCAaCC9d56B9516dF1D100C3E170d05c918C7d3c2", four.Forwarder.Hex()) + assert.Equal(t, "0xb465c41F08d657bAEE5f4771B98a538Ebd65312b", four.BalanceReader.Hex()) +} diff --git a/chain_capabilities/evm/simulated/signers.go b/chain_capabilities/evm/simulated/signers.go new file mode 100644 index 000000000..8a4083f67 --- /dev/null +++ b/chain_capabilities/evm/simulated/signers.go @@ -0,0 +1,33 @@ +package simulated + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +// signers are the oracles of an embedded run, as the forwarder lists them. +// +// They are the onchain halves of the same derived OCR keys the instances sign +// reports with, so the forwarder accepts exactly what this run produces and +// nothing else. An embedded instance's identity is its index, here as everywhere +// else in an embedded run. +func signers(instances int) ([]common.Address, error) { + addresses := make([]common.Address, 0, instances) + for instance := range instances { + bundle, err := ocr.EmbeddedOCR2Bundle(instance) + if err != nil { + return nil, fmt.Errorf("failed to read the OCR key of instance %d: %w", instance, err) + } + // An EVM bundle's public key is the address its signatures recover to, which is + // what the forwarder compares against. + key := bundle.PublicKey() + if len(key) != common.AddressLength { + return nil, fmt.Errorf("the OCR key of instance %d is %d bytes, want an address", instance, len(key)) + } + addresses = append(addresses, common.BytesToAddress(key)) + } + return addresses, nil +} diff --git a/chain_capabilities/evm/trigger/org_context_test.go b/chain_capabilities/evm/trigger/org_context_test.go index 45ee8a7e5..b93b698e8 100644 --- a/chain_capabilities/evm/trigger/org_context_test.go +++ b/chain_capabilities/evm/trigger/org_context_test.go @@ -9,13 +9,14 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" "github.com/smartcontractkit/chainlink-common/pkg/contexts" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services/orgresolver" "github.com/smartcontractkit/chainlink-common/pkg/settings" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" + evmcappb "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/monitoring" ) diff --git a/chain_capabilities/evm/trigger/trigger.go b/chain_capabilities/evm/trigger/trigger.go index fa8bbda80..220c14bfa 100644 --- a/chain_capabilities/evm/trigger/trigger.go +++ b/chain_capabilities/evm/trigger/trigger.go @@ -19,7 +19,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" - evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" evmservice "github.com/smartcontractkit/chainlink-common/pkg/chains/evm" "github.com/smartcontractkit/chainlink-common/pkg/custmsg" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -35,6 +34,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/workflows" "github.com/smartcontractkit/chainlink-common/pkg/workflows/events" + evmcappb "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" + "github.com/smartcontractkit/capabilities/chain_capabilities/evm/monitoring" ) diff --git a/chain_capabilities/evm/trigger/trigger_test.go b/chain_capabilities/evm/trigger/trigger_test.go index 9cccae9a0..fcdfdf5a7 100644 --- a/chain_capabilities/evm/trigger/trigger_test.go +++ b/chain_capabilities/evm/trigger/trigger_test.go @@ -28,7 +28,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - evmcappb "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" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" @@ -36,6 +35,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/query" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives/evm" + + evmcappb "github.com/smartcontractkit/capabilities/chain_capabilities/evm/protos" ) // testLogTriggerCapabilityID matches the shape of chain_capabilities/evm main.go (evm:ChainSelector:@1.0.0 (). diff --git a/consensus/action/capability.go b/consensus/action/capability.go index 55d75eb16..3bd4050f1 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,66 @@ 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] + + // 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 + + 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 +165,78 @@ 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, + 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 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 +544,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 +566,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..2cabb757e 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,134 @@ require ( ) require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/XSAM/otelsql v0.37.0 // indirect + filippo.io/edwards25519 v1.2.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.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/bits-and-blooms/bitset v1.24.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/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/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/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 + 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/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // 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/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/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/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 - 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/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 github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.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/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/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/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 + 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 + go.mongodb.org/mongo-driver v1.17.9 // 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 +158,30 @@ 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 - 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 + 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.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-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 ) + +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/consensus/go.sum b/consensus/go.sum index 38e46af17..3bd7cd8eb 100644 --- a/consensus/go.sum +++ b/consensus/go.sum @@ -1,71 +1,155 @@ 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/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/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/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/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/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.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/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= +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/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/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/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/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= +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.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= +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/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +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/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/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/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= 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-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/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -82,8 +166,12 @@ 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/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= @@ -94,29 +182,57 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN 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 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/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/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/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-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-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/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/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/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,8 +241,12 @@ 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/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.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= @@ -136,22 +256,23 @@ 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/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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/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= @@ -160,30 +281,58 @@ 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-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/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +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 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/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= +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/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= @@ -196,45 +345,70 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +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/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/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/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.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/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/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/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/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= -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= @@ -244,44 +418,64 @@ 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/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/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/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +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.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= @@ -305,15 +499,19 @@ 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= -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= @@ -323,8 +521,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= @@ -334,19 +530,20 @@ 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= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -359,19 +556,17 @@ 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.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/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/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= @@ -382,14 +577,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= @@ -398,17 +589,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= @@ -425,7 +616,11 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/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/yaml.v2 v2.2.2/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/consensus/main.go b/consensus/main.go index 92c68f15d..0825d802a 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/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 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.`, + } + 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, + // 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: factories.Bootstrappers, + 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. +// +// 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"` } + +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..7e3d714c4 --- /dev/null +++ b/consensus/protos/consensus_server_gen.go @@ -0,0 +1,147 @@ +// 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" + + "google.golang.org/protobuf/reflect/protoreflect" + + "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" + +// 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) +} + +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/dispatcher_service.go b/crecore/dispatcher_service.go new file mode 100644 index 000000000..90361deb9 --- /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/rage" + don2don "github.com/smartcontractkit/capabilities/libs/x/don2don" + xrage "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 *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 *rage.Factories, 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 := xrage.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 rage.Factories's peer group factory and identity into xrage.PeerSource. +type peerSource struct { + factories *rage.Factories +} + +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/docs/CONFIG.md b/crecore/docs/CONFIG.md new file mode 100644 index 000000000..3ee2f317b --- /dev/null +++ b/crecore/docs/CONFIG.md @@ -0,0 +1,377 @@ +# main Configuration + +## Example + +```toml +# ----- Global Configuration ----- +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 +[capabilities-registry] +address = '0xYourRegistryAddress' +[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' +delta-dial = '5s' +incoming-buffer-size = 100 +outgoing-buffer-size = 100 +keystore-password = 'xxxxx' +[database] +url = 'postgresql://user:password@localhost:5432/chainlink?sslmode=disable' + + +``` + +## Global +```toml +capabilities-registry-sync-interval = '12s' # Default +``` + + +# Global Configuration + +### capabilities-registry-sync-interval +```toml +capabilities-registry-sync-interval = '12s' # Default +``` +capabilities-registry-sync-interval how often the on-chain registry is re-read + +## telemetry +```toml +[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 +``` + + +### endpoint +```toml +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 + +### ca-cert-file +```toml +ca-cert-file = '' # Default +``` +ca-cert-file CA certificate file used to verify the telemetry endpoint + +### attributes +```toml +attributes = ['env=staging'] # Example +``` +attributes extra telemetry resource attributes, as key=value pairs + +### auth-headers +```toml +auth-headers = [] # Docs only +``` +auth-headers telemetry auth headers, as key=value pairs + +### auth-pub-key-hex +```toml +auth-pub-key-hex = '' # Default +``` +auth-pub-key-hex public key the telemetry auth headers are derived from + +### auth-headers-ttl +```toml +auth-headers-ttl = '0s' # Default +``` +auth-headers-ttl how long generated telemetry auth headers are valid for + +### prometheus-bridge-enabled +```toml +prometheus-bridge-enabled = false # Default +``` +prometheus-bridge-enabled feed metrics registered on the prometheus registry into the telemetry pipeline + +## tracing +```toml +[tracing] +enabled = false # Default +sampling-ratio = 1 # Default +tls-cert-file = '' # Default +``` + + +### enabled +```toml +enabled = false # Default +``` +enabled export traces to the telemetry endpoint + +### sampling-ratio +```toml +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 + +## chip-ingress +```toml +[chip-ingress] +endpoint = '' # Default +insecure-connection = false # Default +``` + + +### 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 +environment = '' # Default +``` +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 + +## capabilities-registry +```toml +[capabilities-registry] +address = '0xYourRegistryAddress' # Example +``` + + +### address +```toml +address = '0xYourRegistryAddress' # Example +``` +address of the on-chain CapabilitiesRegistry (v2) contract + +## 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 + +## 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/endpoint2_server.go b/crecore/endpoint2_server.go new file mode 100644 index 000000000..e32719b8e --- /dev/null +++ b/crecore/endpoint2_server.go @@ -0,0 +1,248 @@ +package main + +import ( + "errors" + "fmt" + "io" + "math" + "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 errors.Is(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: + // 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, to) + } + 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 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: + 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..bc78f3048 --- /dev/null +++ b/crecore/go.mod @@ -0,0 +1,245 @@ +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-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 + 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 + golang.org/x/crypto v0.53.0 + google.golang.org/grpc v1.82.1 +) + +require ( + filippo.io/edwards25519 v1.2.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.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/bits-and-blooms/bitset v1.24.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/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 + 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/consensys/gnark-crypto v0.20.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // 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/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/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/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-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.30.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // 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/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.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.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-bexpr v0.1.10 // 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/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/jhump/protoreflect v1.18.0 // indirect + github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // 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 + github.com/lib/pq v1.12.3 // indirect + 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/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/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.3.1 // 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 + 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/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/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.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 + 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 + github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect + github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // 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/spiffe/go-spiffe/v2 v2.6.0 // 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/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 + go.mongodb.org/mongo-driver v1.17.9 // 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 + 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.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.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 + 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.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/exp v0.0.0-20260508232706-74f9aab9d74a // 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 + 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 +) + +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 + +// 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 new file mode 100644 index 000000000..a8ad52ab6 --- /dev/null +++ b/crecore/go.sum @@ -0,0 +1,907 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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/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.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/XSAM/otelsql v0.42.0 h1:Li0xF4eJUxG2e0x3D4rvRlys1f27yJKvjTh7ljkUP5o= +github.com/XSAM/otelsql v0.42.0/go.mod h1:4mOrEv+cS1KmKzrvTktvJnstr5GtKSAK+QHvFR9OcpI= +github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= +github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +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/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/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/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.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/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-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= +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/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/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/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/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.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= +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.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +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/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/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/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/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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +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.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.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/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/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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.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= +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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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 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/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= +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.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/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-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-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/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/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/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/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= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +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= +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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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.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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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= +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.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/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.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.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/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/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +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= +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/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/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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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/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/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.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-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= +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/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= +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= +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 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-20190812154241-14fe0d1b01d4/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.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +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/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/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/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +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/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.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.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.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-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/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.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= +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/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.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= +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.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.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 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/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/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= +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.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= +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.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.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.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.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.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.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= +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.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/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.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.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-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-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.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.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= +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.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.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/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-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-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-20201021035429-f5854403a974/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-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +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.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-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.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= +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= +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-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-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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.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.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.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= +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/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.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= +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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +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= +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-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-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.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/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-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= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +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= diff --git a/crecore/keystore_server.go b/crecore/keystore_server.go new file mode 100644 index 000000000..71ad9dac2 --- /dev/null +++ b/crecore/keystore_server.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + "github.com/smartcontractkit/chainlink-common/keystore/ocr2offchain" + "github.com/smartcontractkit/chainlink-common/keystore/ragep2p" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + "github.com/smartcontractkit/capabilities/crecore/nodekeys" +) + +// keystoreServer signs with the node's chain keys for capabilities that hold +// none. +// +// It is the signer server's trade for the other half of what a node's identity +// can do: a chain capability transmits as this node, which means signing as the +// account the on-chain registry lists as this node's transmitter. That account's +// key is in the keystore this process unlocked, and stays there - what crosses +// the wire is a digest going out and a signature coming back. +type keystoreServer struct { + creproxy.UnimplementedKeystoreServer + + keystore core.Keystore +} + +var _ creproxy.KeystoreServer = (*keystoreServer)(nil) + +func newKeystoreServer(keystore core.Keystore) *keystoreServer { + return &keystoreServer{keystore: keystore} +} + +// protocolNamespaces are the keys this process runs protocols with rather than +// holds accounts under: the peer identity and the two halves of the OCR keyring. +// +// The keystore is one store of named keys, so a node's chain accounts sit in it +// beside these. They are not accounts and must not be offered as ones: a caller +// listing accounts is asking what it may transmit as, and a chain reading +// "ocr2_offchain/ocr2/ocr2_offchain_encryption" back as an address fails to start. +// Signing with them is worse - the OCR keys are reached through the signer +// service, which binds a signature to a report and a config digest, and signing a +// digest with them here would be a way around that. +var protocolNamespaces = []string{ + ragep2p.PrefixPeerKeyring, + ocr2offchain.PrefixOCR2Offchain, + nodekeys.PrefixOCR2Onchain, +} + +// isProtocolKey reports whether name is one of those, by its leading path +// segment: the keystore names keys as "/"-joined paths, and these three own the +// namespaces they are the root of. +func isProtocolKey(name string) bool { + for _, namespace := range protocolNamespaces { + if name == namespace || strings.HasPrefix(name, namespace+"/") { + return true + } + } + return false +} + +// Accounts is the accounts this node can transmit as: every key in the keystore +// that is not one of the protocol's own. +func (s *keystoreServer) Accounts(ctx context.Context, _ *creproxy.AccountsRequest) (*creproxy.AccountsReply, error) { + all, err := s.keystore.Accounts(ctx) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + accounts := make([]string, 0, len(all)) + for _, account := range all { + if isProtocolKey(account) { + continue + } + accounts = append(accounts, account) + } + return &creproxy.AccountsReply{Accounts: accounts}, nil +} + +// Sign signs the digest it is given with the named account's key. +// +// An unknown account is the caller's mistake rather than this process's failure - +// it is transmitting from an account this node does not hold - so it comes back +// as InvalidArgument, which is the difference between "fix your configuration" +// and "retry". +func (s *keystoreServer) Sign(ctx context.Context, req *creproxy.SignRequest) (*creproxy.SignReply, error) { + if req.GetAccount() == "" { + return nil, status.Error(codes.InvalidArgument, "no account to sign with") + } + // A protocol key is not an account, and this is not the way to sign with one: the + // signer service signs reports with the OCR keys, over a config digest and a + // sequence number, and this would sign whatever bytes it was handed. + if isProtocolKey(req.GetAccount()) { + return nil, status.Errorf(codes.PermissionDenied, "%s is one of this node's protocol keys, not an account it transmits from", req.GetAccount()) + } + + // No data is the existence check the core.Keystore interface describes, which a + // node's own keystore answers with no signature rather than a signature of + // nothing. Answered here for the same reason: what asks is chainlink-evm, which + // asks a node's keystore the same question. + if len(req.GetData()) == 0 { + accounts, err := s.keystore.Accounts(ctx) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + for _, account := range accounts { + if strings.EqualFold(account, req.GetAccount()) { + return &creproxy.SignReply{}, nil + } + } + return nil, status.Errorf(codes.InvalidArgument, "this node holds no key for account %s", req.GetAccount()) + } + + signed, err := s.keystore.Sign(ctx, req.GetAccount(), req.GetData()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &creproxy.SignReply{Signed: signed}, nil +} diff --git a/crecore/keystore_server_test.go b/crecore/keystore_server_test.go new file mode 100644 index 000000000..8b9909178 --- /dev/null +++ b/crecore/keystore_server_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// fakeKeystore is a node's keystore as this server sees one: named keys, some of +// them accounts and some of them the protocol's. +type fakeKeystore struct { + names []string + signed string +} + +var _ core.Keystore = (*fakeKeystore)(nil) + +func (f *fakeKeystore) Accounts(context.Context) ([]string, error) { return f.names, nil } + +func (f *fakeKeystore) Decrypt(context.Context, string, []byte) ([]byte, error) { + return nil, errors.New("this server serves no decryption") +} + +func (f *fakeKeystore) Sign(_ context.Context, account string, _ []byte) ([]byte, error) { + f.signed = account + return []byte("signature"), nil +} + +const ( + account = "0x1234567890123456789012345678901234567890" + peerKey = "ragep2p_peer/p2p" + offchain = "ocr2_offchain/ocr2/ocr2_offchain_encryption" + onchain = "ocr2_onchain/ocr2/ocr2_onchain_signing" + otherAcct = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" +) + +// TestAccountsAreAccounts covers what a chain does with this list: it reads every +// name back as an address, so a protocol key among them is not a key it ignores +// but a chain that will not start. +func TestAccountsAreAccounts(t *testing.T) { + ks := &fakeKeystore{names: []string{peerKey, account, offchain, onchain, otherAcct}} + + reply, err := newKeystoreServer(ks).Accounts(t.Context(), &creproxy.AccountsRequest{}) + require.NoError(t, err) + assert.Equal(t, []string{account, otherAcct}, reply.GetAccounts()) +} + +// TestSignRefusesProtocolKeys is the other half, and the one that matters: the +// OCR keys are reached through the signer service, which signs a report under a +// config digest and a sequence number. Signing arbitrary bytes with them here +// would be a way around that. +func TestSignRefusesProtocolKeys(t *testing.T) { + for _, name := range []string{peerKey, offchain, onchain} { + t.Run(name, func(t *testing.T) { + ks := &fakeKeystore{names: []string{name}} + + _, err := newKeystoreServer(ks).Sign(t.Context(), &creproxy.SignRequest{Account: name, Data: []byte("anything")}) + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + assert.Empty(t, ks.signed, "the key must not have been reached at all") + }) + } +} + +// TestSignExistenceCheck mirrors what a node's own keystore answers: no data is +// the existence check the interface describes, and the answer is no signature +// rather than a signature of nothing. +func TestSignExistenceCheck(t *testing.T) { + ks := &fakeKeystore{names: []string{account}} + server := newKeystoreServer(ks) + + reply, err := server.Sign(t.Context(), &creproxy.SignRequest{Account: account}) + require.NoError(t, err) + assert.Empty(t, reply.GetSigned()) + assert.Empty(t, ks.signed, "nothing was signed") + + _, err = server.Sign(t.Context(), &creproxy.SignRequest{Account: otherAcct}) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) +} + +func TestSignWithAnAccount(t *testing.T) { + ks := &fakeKeystore{names: []string{account}} + + reply, err := newKeystoreServer(ks).Sign(t.Context(), &creproxy.SignRequest{Account: account, Data: []byte("a digest")}) + require.NoError(t, err) + assert.Equal(t, []byte("signature"), reply.GetSigned()) + assert.Equal(t, account, ks.signed) +} diff --git a/crecore/main.go b/crecore/main.go new file mode 100644 index 000000000..43f67a852 --- /dev/null +++ b/crecore/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "database/sql" + "embed" + "log" + + "github.com/jmoiron/sqlx" + "github.com/spf13/cobra" + + "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/rage" + + "github.com/smartcontractkit/capabilities/crecore/nodekeys" + "github.com/smartcontractkit/capabilities/libs/x/registry" + "github.com/smartcontractkit/capabilities/libs/x/registrysyncer" + + "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" +) + +//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" + +// 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" + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + 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 the node's own, loaded from the keystore in +the database the two share. + +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.`, + } + root.PersistentFlags().String("config", "", "Path to config file") + + if err := flags.RegisterCommandFlags(root, &cfg, flags.DefaultTOMLOptions("CRE", "CL")); err != nil { + return err + } + + bootstrapper := standalone.NewBootstrapper(root, standalone.WithOtelViews(metricViews())) + lggr := bootstrapper.Logger() + + dbDep := db.Dependency(embeddedMigrations, migrationsTable) + // This process's keys: the node's, or derived ones under embed. The peer is handed to the rage + // host as the identity to announce under; the rest is what this process signs with on behalf of + // capabilities. + keysDep := nodekeys.Dependency(lggr.Named("Keystore"), dbDep) + ocrDep := rage.Host(lggr.Named("OCR"), dbDep, ocrDiscovererTable, nodekeys.PeerKeyring(keysDep)) + + readerDep := evmregistry.Dependency(lggr.Named("CapabilitiesRegistry"), evm.Dependency(lggr.Named("EVM"))) + + grpcDep := standalonegrpc.Dependency(lggr.Named("CoreAPI")) + + return standalone.Run5(bootstrapper, func( + ctx context.Context, + scfg *standalone.StandaloneConfig, + factories *rage.Factories, + keys nodekeys.Keys, + 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 + // 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) + + // 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, grpcSrv.Registrar()) + proxySvc := newProxyService(scfg.Logger.Named("proxy service"), grpcSrv.Registrar(), factories, keys) + + svcs := []services.Service{proxySvc, regSvc} + + // 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. It needs a peer group, which only a + // real hosted peer has: an embedded run's peers are goroutines reached over channels, with + // no group to join, so there it is left out rather than started to fail. + if factories.PeerGroup != nil { + svcs = append(svcs, newDispatcherService(cfg.Dispatcher, scfg.Logger.Named("dispatcher"), factories, regSvc.CapabilitiesRegistry())) + } else { + scfg.Logger.Infow("No peer group behind this process, so DON-to-DON messaging is not served") + } + + // 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 append(svcs, grpcSrv) + }, ocrDep, keysDep, readerDep, dbDep, grpcDep) +} diff --git a/crecore/metrics.go b/crecore/metrics.go new file mode 100644 index 000000000..1f3eb3d85 --- /dev/null +++ b/crecore/metrics.go @@ -0,0 +1,75 @@ +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" + + 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/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/nodekeys/nodekeys.go b/crecore/nodekeys/nodekeys.go new file mode 100644 index 000000000..875c51029 --- /dev/null +++ b/crecore/nodekeys/nodekeys.go @@ -0,0 +1,281 @@ +// Package nodekeys is this process's keys: the node's, unlocked from the keystore +// they share, or - for an embedded run, which has no node - derived from the +// instance index. +// +// It lives here rather than in libs because this is the only binary that holds +// keys. Everything else in this framework is on the other side of that: a +// capability asks this process to sign (see libs/standalone/keystore), and what it +// gets is a signature, never a key. +// +// Nothing is looked up until it is asked for. Unlocking the store says the +// password is right and the database is reachable; which keys are in it is only +// discovered by whoever wants one, so a node with no OCR key still serves its peer +// and its chain accounts, and says what is missing to the caller that needed it. +package nodekeys + +import ( + "context" + "database/sql" + "errors" + "fmt" + "sync" + + "github.com/jmoiron/sqlx" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/keystore" + "github.com/smartcontractkit/chainlink-common/keystore/ocr2offchain" + "github.com/smartcontractkit/chainlink-common/keystore/pgstore" + "github.com/smartcontractkit/chainlink-common/keystore/ragep2p" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +// namespace groups these settings under keystore.*: they are about keys, not about +// the peer that happens to use one of them. +const namespace = "keystore" + +// evmFamily is the signing family the OCR keyring announces under. The +// configurations this process signs for are the capabilities registry's, which +// lists members by family, and this node signs as an EVM one. +const evmFamily = ocr.EVMFamily + +// Config is which keystore, and which keys in it. +// +// The keystore names keys rather than typing them, so which key is the peer is a +// convention. These are the conventional names, and the same ones the node's own +// bootstrap copies its keys under; a deployment that chose otherwise says so here. +// +// Nothing is `validate:"required"`: an embedded instance derives its keys and is +// told none of this, so the rules are checked when the configured form resolves. +type Config struct { + // Password unlocks the keystore. Typed as a SecretString so it redacts itself in + // logs, docs and generated example configs. + Password commonconfig.SecretString `usage:"password for the node's keystore, which holds its P2P identity, OCR keys and chain keys; required unless the keys are derived, as they are under embed"` + + // Name is the keystore's row in the shared database. A database holds one per + // process that has keys of its own, so this names the node's. + Name string `usage:"name of the node's keystore in the shared database"` + + // PeerKey and OCRKey are the keys in it this process uses. + PeerKey string `usage:"name of the node's rage P2P key in its keystore"` + OCRKey string `usage:"name of the node's OCR keyring in its keystore, whose keys this process signs rounds with"` +} + +// Defaults are the conventional names, used for whatever is left unnamed. +var Defaults = Config{Name: "node", PeerKey: "p2p", OCRKey: "ocr2"} + +func (c Config) withDefaults() Config { + if c.Name == "" { + c.Name = Defaults.Name + } + if c.PeerKey == "" { + c.PeerKey = Defaults.PeerKey + } + if c.OCRKey == "" { + c.OCRKey = Defaults.OCRKey + } + return c +} + +// Keys is what this process can sign with, asked one key at a time. +// +// Each method looks its key up when it is called and remembers what it found, so a +// key nothing uses is never read and a key that is not there is reported to the +// caller that wanted it rather than to everything at startup. +type Keys interface { + // Peer is the identity the rage peer announces under. Other DON members expect + // this node's peer ID at this process's address. + Peer(ctx context.Context) (ragetypes.PeerKeyring, error) + + // OCR is the identity this process signs rounds with on behalf of oracles that + // hold no keys: the offchain half for every protocol message, the onchain half for + // the report at the end. + OCR(ctx context.Context) (ocr.Keyrings, error) + + // Chain is what a chain capability transmits through, addressed by account name. + // The store types its own keys and this interface does not, so which accounts + // exist is a question the store answers per call - which is why this one needs no + // context and cannot fail. + // + // Nil for an embedded run, which has no node to transmit as. + Chain() core.Keystore +} + +// Dependency returns this process's keys, over the keystore in the database it +// shares with the node. +// +// Resolving it unlocks that keystore - which says the password is right - and +// nothing more: see Keys. +func Dependency(lggr commonlogger.Logger, db standalone.BootstrapDependency[*sql.DB]) standalone.BootstrapDependency[Keys] { + // Wrapped so the keystore is unlocked at most once however many services resolve + // this: unlocking it is scrypt, which is slow on purpose. + return standalone.OnceBootstrapper[Keys](&dependency{lggr: lggr, db: db, cfg: Defaults}) +} + +type dependency struct { + lggr commonlogger.Logger + db standalone.BootstrapDependency[*sql.DB] + cfg Config +} + +var _ standalone.BootstrapDependency[Keys] = (*dependency)(nil) + +func (d *dependency) Namespace() string { return namespace } + +func (d *dependency) Config() any { return &d.cfg } + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{d.db} +} + +// ForEmbedding returns the derived form: an embedded run has no node, and so no +// keystore to unlock and no password to unlock it with. See derived. +func (d *dependency) ForEmbedding(i, _ int) standalone.BootstrapDependency[Keys] { + return &embedded{lggr: d.lggr, index: i} +} + +func (d *dependency) Get(ctx context.Context, cc standalone.CommonConfig) (Keys, error) { + cfg := d.cfg.withDefaults() + if cfg.Password == "" { + return nil, errors.New("--keystore.password is required to unlock the node's keys") + } + + database, err := d.db.Get(ctx, cc) + if err != nil { + return nil, fmt.Errorf("failed to get database: %w", err) + } + + ks, err := keystore.LoadKeystore(ctx, pgstore.NewStorage(sqlx.NewDb(database, "pgx"), cfg.Name), string(cfg.Password)) + if err != nil { + return nil, fmt.Errorf("failed to unlock the keystore %q: %w", cfg.Name, err) + } + + d.lggr.Infow("Unlocked the node's keystore", "keystore", cfg.Name) + + return &nodeKeys{keystore: ks, cfg: cfg}, nil +} + +// nodeKeys is the node's unlocked keystore, read one key at a time. +type nodeKeys struct { + keystore keystore.Keystore + cfg Config + + peer once[ragetypes.PeerKeyring] + ocr once[ocr.Keyrings] +} + +var _ Keys = (*nodeKeys)(nil) + +func (k *nodeKeys) Peer(ctx context.Context) (ragetypes.PeerKeyring, error) { + return k.peer.do(func() (ragetypes.PeerKeyring, error) { + keyrings, err := ragep2p.GetPeerKeyrings(ctx, k.keystore, []string{k.cfg.PeerKey}) + if err != nil { + return nil, fmt.Errorf("failed to read the peer key %q: %w", k.cfg.PeerKey, err) + } + if len(keyrings) != 1 { + return nil, fmt.Errorf("expected one peer key named %q, found %d", k.cfg.PeerKey, len(keyrings)) + } + return keyrings[0], nil + }) +} + +func (k *nodeKeys) OCR(ctx context.Context) (ocr.Keyrings, error) { + return k.ocr.do(func() (ocr.Keyrings, error) { + offchain, err := k.offchain(ctx) + if err != nil { + return ocr.Keyrings{}, err + } + onchain, err := newOnchainKeyring(ctx, k.keystore, k.cfg.OCRKey, evmFamily) + if err != nil { + return ocr.Keyrings{}, err + } + return ocr.Keyrings{Offchain: offchain, Onchain: onchain}, nil + }) +} + +func (k *nodeKeys) offchain(ctx context.Context) (ocrtypes.OffchainKeyring, error) { + keyrings, err := ocr2offchain.GetOCR2OffchainKeyrings(ctx, k.keystore, []string{k.cfg.OCRKey}) + if err != nil { + return nil, fmt.Errorf("failed to read the OCR offchain keys %q: %w", k.cfg.OCRKey, err) + } + if len(keyrings) != 1 { + return nil, fmt.Errorf("expected one OCR offchain keyring named %q, found %d", k.cfg.OCRKey, len(keyrings)) + } + return keyrings[0], nil +} + +// Chain is the whole store: which accounts it holds is what a caller asking for +// one finds out, so there is nothing to look up here. +func (k *nodeKeys) Chain() core.Keystore { return keystore.NewCoreKeystore(k.keystore) } + +// embedded is one embedded instance's keys, derived from its index: there is no +// node behind an embedded run, so there is nothing to borrow an identity from. +// +// Deriving rather than generating is what makes such a run usable: the peer IDs +// and public keys are known before it starts, so the configuration its instances +// form can be computed from the instance count alone. +type embedded struct { + lggr commonlogger.Logger + index int +} + +var _ standalone.BootstrapDependency[Keys] = (*embedded)(nil) + +func (d *embedded) Namespace() string { return namespace } + +// Config is nothing at all: an embedded instance cannot be told a keystore +// password, and there is no keystore to name. +func (d *embedded) Config() any { return nil } + +func (d *embedded) Dependencies() []standalone.BootstrapCommand { return nil } + +func (d *embedded) ForEmbedding(i, _ int) standalone.BootstrapDependency[Keys] { + return &embedded{lggr: d.lggr, index: i} +} + +func (d *embedded) Get(context.Context, standalone.CommonConfig) (Keys, error) { + return &derivedKeys{index: d.index}, nil +} + +// derivedKeys are instance i's, computed from i when asked for. +type derivedKeys struct { + index int + + peer once[ragetypes.PeerKeyring] + ocr once[ocr.Keyrings] +} + +var _ Keys = (*derivedKeys)(nil) + +func (k *derivedKeys) Peer(context.Context) (ragetypes.PeerKeyring, error) { + return k.peer.do(func() (ragetypes.PeerKeyring, error) { return ocr.DeterministicKeyring(k.index) }) +} + +func (k *derivedKeys) OCR(context.Context) (ocr.Keyrings, error) { + return k.ocr.do(func() (ocr.Keyrings, error) { return ocr.EmbeddedKeyrings(k.index) }) +} + +// Chain is nil: an embedded instance transmits as nobody, so it has no account to +// lend a capability. +func (k *derivedKeys) Chain() core.Keystore { return nil } + +// once resolves a key the first time it is wanted and remembers the answer, +// including a failure: a key that is not in the store will not appear between one +// call and the next, and re-reading it per signature would put scrypt on the path +// of every round. +type once[T any] struct { + once sync.Once + value T + err error +} + +func (o *once[T]) do(resolve func() (T, error)) (T, error) { + o.once.Do(func() { o.value, o.err = resolve() }) + return o.value, o.err +} diff --git a/crecore/nodekeys/onchain.go b/crecore/nodekeys/onchain.go new file mode 100644 index 000000000..0aa882ee7 --- /dev/null +++ b/crecore/nodekeys/onchain.go @@ -0,0 +1,152 @@ +package nodekeys + +import ( + "context" + "errors" + "fmt" + + "golang.org/x/crypto/sha3" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/keystore" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" +) + +// OnchainKeyring signs the reports an OCR3 round produces, with a key it never +// holds: the signing is a call into the keystore, which is what lets the key stay +// in the process that unlocked it. +// +// The rules it follows are not its own. What a round's bytes are (ocr2key's +// ReportToSigData over OCR3ReportContext) and how a member's public key is +// written in a configuration (the multichain encoding) are properties of the +// protocol and the registry, shared with every other implementation of them: two +// oracles that disagree about either produce signatures the other rejects, and +// that shows up as a DON which will not come to consensus rather than as anything +// anyone can see. +type OnchainKeyring struct { + keystore keystore.Keystore + keyName string + + // address is this key's EVM address, which is what libocr calls the onchain + // public key for this family. + address []byte + // publicKey is that address in the multichain encoding a configuration lists + // members by, computed once here so a family that cannot be encoded fails while + // the keyring is being built. + publicKey ocrtypes.OnchainPublicKey +} + +var _ ocr3types.OnchainKeyring[[]byte] = (*OnchainKeyring)(nil) + +// The onchain key's path, alongside the offchain keys chainlink-common's +// ocr2offchain puts under the same keyring name. +// +// Spelled here and in whatever writes the key - for a node, chainlink's keyseed +// package - because the two are different repositories. A disagreement is a key +// this cannot find, which is a startup error rather than a bad signature. +const ( + // PrefixOCR2Onchain namespaces the onchain key, the way chainlink-common's own + // packages namespace theirs. Exported because it is also what says this key is + // the protocol's rather than a chain account: see crecore's keystore server. + PrefixOCR2Onchain = "ocr2_onchain" + + onchainSigning = "ocr2_onchain_signing" +) + +// onchainKeyName is where the onchain key sits relative to the OCR keyring's name. +func onchainKeyName(keyring string) string { + return keystore.NewKeyPath(PrefixOCR2Onchain, keyring, onchainSigning).String() +} + +func newOnchainKeyring(ctx context.Context, ks keystore.Keystore, keyring, family string) (*OnchainKeyring, error) { + if family == "" { + return nil, errors.New("an onchain keyring must say which signing family it is for") + } + + name := onchainKeyName(keyring) + keys, err := ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: []string{name}}) + if err != nil { + return nil, fmt.Errorf("failed to read the OCR onchain key %q: %w", name, err) + } + if len(keys.Keys) != 1 { + return nil, fmt.Errorf("expected one OCR onchain key named %q, found %d", name, len(keys.Keys)) + } + + address, err := evmAddress(keys.Keys[0].KeyInfo.PublicKey) + if err != nil { + return nil, fmt.Errorf("the OCR onchain key %q is not a secp256k1 key: %w", name, err) + } + + publicKey, err := ocr2key.MarshalMultichainPublicKey(map[string]ocrtypes.OnchainPublicKey{family: address}) + if err != nil { + return nil, fmt.Errorf("failed to encode the %s onchain public key: %w", family, err) + } + if len(publicKey) == 0 { + return nil, fmt.Errorf("%q is not a known signing family, so a keyring for it would announce nothing", family) + } + + return &OnchainKeyring{keystore: ks, keyName: name, address: address, publicKey: publicKey}, nil +} + +// PublicKey is what a configuration lists this oracle under. +func (k *OnchainKeyring) PublicKey() ocrtypes.OnchainPublicKey { return k.publicKey } + +// MaxSignatureLength is what libocr budgets for a signature from this keyring: a +// secp256k1 signature with its recovery byte. +func (k *OnchainKeyring) MaxSignatureLength() int { return 65 } + +// Sign signs a round's report. The digest it signs is the protocol's, not this +// keyring's: see ocr2key.ReportToSigData. +func (k *OnchainKeyring) Sign(digest ocrtypes.ConfigDigest, seqNr uint64, report ocr3types.ReportWithInfo[[]byte]) ([]byte, error) { + signed, err := k.keystore.Sign(context.Background(), keystore.SignRequest{ + KeyName: k.keyName, + Data: ocr2key.ReportToSigData(ocr2key.OCR3ReportContext(digest, seqNr), report.Report), + }) + if err != nil { + return nil, fmt.Errorf("failed to sign the report: %w", err) + } + return signed.Signature, nil +} + +// Verify checks a peer's signature. It needs no key of this node's, so it is done +// here rather than asked of anything: every message received would otherwise cost +// a round trip. +func (k *OnchainKeyring) Verify(publicKey ocrtypes.OnchainPublicKey, digest ocrtypes.ConfigDigest, seqNr uint64, report ocr3types.ReportWithInfo[[]byte], signature []byte) bool { + keys, err := ocr2key.UnmarshalMultichainPublicKey(publicKey) + if err != nil { + // Not multichain: a member listed by its bare key for this family. + keys = map[string]ocrtypes.OnchainPublicKey{} + } + + blob := ocr2key.ReportToSigData(ocr2key.OCR3ReportContext(digest, seqNr), report.Report) + if len(keys) == 0 { + return ocr2key.EvmVerifyBlob(publicKey, blob, signature) + } + for _, key := range keys { + if ocr2key.EvmVerifyBlob(key, blob, signature) { + return true + } + } + return false +} + +// evmAddress is the address of a secp256k1 public key: the last 20 bytes of the +// keccak256 of its uncompressed form, minus the leading tag byte. +// +// It is computed here rather than taken from a chain's library because this +// package is deliberately chain-agnostic, and because the store hands back exactly +// the uncompressed form this needs. +func evmAddress(publicKey []byte) ([]byte, error) { + const uncompressedLength = 65 + if len(publicKey) != uncompressedLength { + return nil, fmt.Errorf("public key is %d bytes, want %d", len(publicKey), uncompressedLength) + } + + hash := sha3.NewLegacyKeccak256() + if _, err := hash.Write(publicKey[1:]); err != nil { + return nil, err + } + return hash.Sum(nil)[12:], nil +} diff --git a/crecore/nodekeys/onchain_test.go b/crecore/nodekeys/onchain_test.go new file mode 100644 index 000000000..69143fd16 --- /dev/null +++ b/crecore/nodekeys/onchain_test.go @@ -0,0 +1,90 @@ +package nodekeys + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-common/keystore" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" +) + +// TestOnchainKeyring_InteropWithKeyBundle is the test that matters: a signature +// this keyring makes with a key in the new keystore has to be one an oracle +// holding a legacy key bundle accepts, and the other way round. The two are +// members of the same DON, so what they disagree about is not a bug that shows up +// as a bug - it shows up as a DON that will not agree. +func TestOnchainKeyring_InteropWithKeyBundle(t *testing.T) { + const family = "evm" + + ctx := t.Context() + digest := ocrtypes.ConfigDigest{1, 2, 3} + const seqNr = uint64(42) + report := ocr3types.ReportWithInfo[[]byte]{Report: []byte("a report")} + + // This node: a key in the new keystore, signed for through it. + ks, err := keystore.LoadKeystore(ctx, keystore.NewMemoryStorage(), "password") + require.NoError(t, err) + _, err = ks.CreateKeys(ctx, keystore.CreateKeysRequest{Keys: []keystore.CreateKeyRequest{{ + KeyName: onchainKeyName("ocr2"), + KeyType: keystore.ECDSA_S256, + }}}) + require.NoError(t, err) + + mine, err := newOnchainKeyring(ctx, ks, "ocr2", family) + require.NoError(t, err) + + // Another member: a legacy key bundle, which is what a node still holds. + bundle, err := ocr2key.New(corekeys.EVM) + require.NoError(t, err) + theirKey, err := ocr2key.MarshalMultichainPublicKey(map[string]ocrtypes.OnchainPublicKey{family: bundle.PublicKey()}) + require.NoError(t, err) + + t.Run("a key bundle accepts what this keyring signed", func(t *testing.T) { + signature, err := mine.Sign(digest, seqNr, report) + require.NoError(t, err) + require.Len(t, signature, mine.MaxSignatureLength()) + + // What the bundle-holding member does with the signature and this node's + // advertised public key. + keys, err := ocr2key.UnmarshalMultichainPublicKey(mine.PublicKey()) + require.NoError(t, err, "the public key this keyring announces must be the encoding a configuration lists") + require.Contains(t, keys, family) + + blob := ocr2key.ReportToSigData(ocr2key.OCR3ReportContext(digest, seqNr), report.Report) + assert.True(t, ocr2key.EvmVerifyBlob(keys[family], blob, signature)) + }) + + t.Run("this keyring accepts what a key bundle signed", func(t *testing.T) { + signature, err := bundle.Sign(ocr2key.OCR3ReportContext(digest, seqNr), report.Report) + require.NoError(t, err) + + assert.True(t, mine.Verify(theirKey, digest, seqNr, report, signature)) + }) + + t.Run("a signature over another round is refused", func(t *testing.T) { + signature, err := mine.Sign(digest, seqNr, report) + require.NoError(t, err) + + assert.False(t, mine.Verify(mine.PublicKey(), digest, seqNr+1, report, signature)) + assert.False(t, mine.Verify(mine.PublicKey(), ocrtypes.ConfigDigest{9}, seqNr, report, signature)) + assert.False(t, mine.Verify(theirKey, digest, seqNr, report, signature), + "another member's key must not verify this node's signature") + }) + + // Pinned: whatever puts the key in the store has to use this exact name, and it is + // in another repository (chainlink's keyseed package). + t.Run("the onchain key's name is the agreed one", func(t *testing.T) { + assert.Equal(t, "ocr2_onchain/ocr2/ocr2_onchain_signing", onchainKeyName("ocr2")) + }) + + t.Run("a keyring with no family announces nothing", func(t *testing.T) { + _, err := newOnchainKeyring(ctx, ks, "ocr2", "") + assert.ErrorContains(t, err, "must say which signing family") + }) +} diff --git a/crecore/nodekeys/peer.go b/crecore/nodekeys/peer.go new file mode 100644 index 000000000..f78f129a0 --- /dev/null +++ b/crecore/nodekeys/peer.go @@ -0,0 +1,47 @@ +package nodekeys + +import ( + "context" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" +) + +// PeerKeyring is the peer half of keys, on its own. +// +// It exists because the peer is resolved by something that should know nothing +// about keystores: libs/standalone/rage listens, dials and announces, and is +// handed the identity to announce under. This is the adapter between the two - +// the same keys, seen through the one field a peer needs. +func PeerKeyring(keys standalone.BootstrapDependency[Keys]) standalone.BootstrapDependency[ragetypes.PeerKeyring] { + return &peerKeyringDependency{keys: keys} +} + +type peerKeyringDependency struct { + keys standalone.BootstrapDependency[Keys] +} + +var _ standalone.BootstrapDependency[ragetypes.PeerKeyring] = (*peerKeyringDependency)(nil) + +// Namespace is empty and Config is nil: this adds no settings of its own, and the +// keys it narrows have already registered theirs. +func (d *peerKeyringDependency) Namespace() string { return "" } + +func (d *peerKeyringDependency) Config() any { return nil } + +func (d *peerKeyringDependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{d.keys} +} + +func (d *peerKeyringDependency) ForEmbedding(i, instances int) standalone.BootstrapDependency[ragetypes.PeerKeyring] { + return &peerKeyringDependency{keys: d.keys.ForEmbedding(i, instances)} +} + +func (d *peerKeyringDependency) Get(ctx context.Context, cc standalone.CommonConfig) (ragetypes.PeerKeyring, error) { + keys, err := d.keys.Get(ctx, cc) + if err != nil { + return nil, err + } + return keys.Peer(ctx) +} diff --git a/crecore/proxy_service.go b/crecore/proxy_service.go new file mode 100644 index 000000000..8235df7f2 --- /dev/null +++ b/crecore/proxy_service.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + + "github.com/smartcontractkit/capabilities/libs/standalone/rage" + + "github.com/smartcontractkit/capabilities/crecore/nodekeys" + "github.com/smartcontractkit/capabilities/libs/x/registry" + + "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 root command's configuration, populated by flags.RegisterCommandFlags (see +// 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"` + + // 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 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 + eng *services.Engine + + lggr logger.Logger + // 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 *rage.Factories + // keys are what this process signs with on behalf of oracles and capabilities that hold none. + keys nodekeys.Keys +} + +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 *rage.Factories, keys nodekeys.Keys) *proxyService { + s := &proxyService{lggr: lggr, grpcServer: grpcServer, factories: factories, keys: keys} + s.Service, s.eng = services.Config{ + Name: "P2PProxy", + Start: s.start, + }.NewServiceEngine(lggr) + return s +} + +func (s *proxyService) start(context.Context) error { + metrics, err := newProxyMetrics() + if err != nil { + return fmt.Errorf("failed to create proxy metrics: %w", err) + } + + // 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. + // + // Registered without reading a key. Which keys are there is what a caller asking + // for one finds out, so a node with no OCR key still proxies its peer and lends + // its chain accounts. + creproxy.RegisterSignerServer(s.grpcServer, newSignerServer(s.keys)) + + // Chain keys go on the same surface for the same reason, when the node has any: + // a chain capability transmits as this node, and this is the process that can + // sign as it. A node holding no EVM keys serves nothing here, so a capability + // pointed at it fails when it dials rather than when it first transmits. + // Nil only for an embedded run, which has no node keystore behind it: its + // instances derive what they sign with, so there is nothing to lend them. + if chain := s.keys.Chain(); chain != nil { + creproxy.RegisterKeystoreServer(s.grpcServer, newKeystoreServer(chain)) + } else { + s.eng.Infow("No node keystore behind this process, so no chain signing is served") + } + + return nil +} diff --git a/crecore/registry_service.go b/crecore/registry_service.go new file mode 100644 index 000000000..f089752ca --- /dev/null +++ b/crecore/registry_service.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/capabilities/libs/x/registry" + + "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 +// 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. +// +// 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 + + // syncInterval is how often the registry is re-read; see main.go. + syncInterval time.Duration + + 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 *registry.Registry +} + +var _ services.Service = (*registryService)(nil) + +func newRegistryService( + syncInterval time.Duration, + lggr logger.Logger, + reader registry.Reader, + orm registry.ORM, + peerID ragetypes.PeerID, + grpcServer grpc.ServiceRegistrar, +) *registryService { + syncer := registry.NewSyncer(lggr, reader, orm, + peerID, syncInterval) + + s := ®istryService{ + syncInterval: syncInterval, + lggr: lggr, + peerID: peerID, + syncer: syncer, + // 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 + // rather than defaulted in the client (mirrors creregistry.Select). + 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) + + // The syncer is a sub-service rather than something this starts by hand, because + // that is what puts its health in this service's report: it is unhealthy until a + // snapshot lands, and a process whose registry never read is one whose every + // lookup fails. Reporting that as healthy would be a lie a node acts on. + s.Service, s.eng = services.Config{ + Name: "CapabilitiesRegistry", + NewSubServices: func(logger.Logger) []services.Service { return []services.Service{syncer} }, + Start: s.start, + }.NewServiceEngine(lggr) + return s +} + +func (s *registryService) start(context.Context) error { + s.lggr.Infow("CapabilitiesRegistry started", + "syncInterval", s.syncInterval, "peerID", s.peerID.String()) + return nil +} + +// 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 s.registry +} diff --git a/crecore/server.go b/crecore/server.go new file mode 100644 index 000000000..a2ba755c5 --- /dev/null +++ b/crecore/server.go @@ -0,0 +1,164 @@ +package main + +import ( + "errors" + "fmt" + "io" + "math" + "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 errors.Is(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: + // 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, to) + 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 +} + +// 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/crecore/signer_server.go b/crecore/signer_server.go new file mode 100644 index 000000000..f01d7019e --- /dev/null +++ b/crecore/signer_server.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + "github.com/smartcontractkit/capabilities/crecore/nodekeys" + "github.com/smartcontractkit/capabilities/libs/standalone/ocr" +) + +// 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 keys, which are +// the ones the registry lists as one of the DON's signers. +// +// It holds the keys rather than the keyrings, and asks for them when a call comes +// in: whether this node has an OCR key is the caller's business to discover, not a +// reason to refuse to start a process that also proxies a peer and lends chain +// accounts. +type signerServer struct { + creproxy.UnimplementedSignerServer + + keys nodekeys.Keys +} + +var _ creproxy.SignerServer = (*signerServer)(nil) + +func newSignerServer(keys nodekeys.Keys) *signerServer { + return &signerServer{keys: keys} +} + +// keyrings is this node's OCR identity, or the reason it has none. +// +// FailedPrecondition rather than Internal: a node with no OCR key is not broken, +// it is a node that was never given one, and the caller is the one that can tell +// the difference. +func (s *signerServer) keyrings(ctx context.Context) (ocr.Keyrings, error) { + keyrings, err := s.keys.OCR(ctx) + if err != nil { + return ocr.Keyrings{}, status.Errorf(codes.FailedPrecondition, "this node has no OCR keys to sign with: %s", err) + } + return keyrings, nil +} + +func (s *signerServer) Keys(ctx context.Context, _ *creproxy.KeysRequest) (*creproxy.KeysReply, error) { + keyrings, err := s.keyrings(ctx) + if err != nil { + return nil, err + } + + offchain := keyrings.Offchain.OffchainPublicKey() + config := keyrings.Offchain.ConfigEncryptionPublicKey() + + return &creproxy.KeysReply{ + OffchainPublicKey: offchain[:], + ConfigEncryptionPublicKey: config[:], + OnchainPublicKey: keyrings.Onchain.PublicKey(), + MaxSignatureLength: uint32(keyrings.Onchain.MaxSignatureLength()), //#nosec G115 - a signature length is small + }, nil +} + +func (s *signerServer) SignOffchain(ctx context.Context, req *creproxy.SignOffchainRequest) (*creproxy.SignatureReply, error) { + keyrings, err := s.keyrings(ctx) + if err != nil { + return nil, err + } + + signature, err := keyrings.Offchain.OffchainSign(req.GetMessage()) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &creproxy.SignatureReply{Signature: signature}, nil +} + +func (s *signerServer) ConfigDiffieHellman(ctx 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()) + + keyrings, err := s.keyrings(ctx) + if err != nil { + return nil, err + } + + shared, err := keyrings.Offchain.ConfigDiffieHellman(point) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &creproxy.ConfigDiffieHellmanReply{SharedSecret: shared[:]}, nil +} + +func (s *signerServer) SignReport(ctx 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()) + + keyrings, err := s.keyrings(ctx) + if err != nil { + return nil, err + } + + // Signed through the keyring rather than by reaching for the key: the oracle + // asking for this signature verifies its peers' with the same keyring, so what a + // round's bytes are is decided in one place. + signature, err := keyrings.Onchain.Sign(digest, req.GetSeqNr(), ocr3types.ReportWithInfo[[]byte]{Report: req.GetReport()}) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &creproxy.SignatureReply{Signature: signature}, nil +} diff --git a/cron/go.mod b/cron/go.mod index d76d4aae6..73964cc07 100644 --- a/cron/go.mod +++ b/cron/go.mod @@ -7,104 +7,126 @@ 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/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/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/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/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 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 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 - 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/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 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/cobra v1.10.2 // 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 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,16 +135,30 @@ 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 - golang.org/x/crypto v0.52.0 // 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.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-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..b6cbe66a7 100644 --- a/cron/go.sum +++ b/cron/go.sum @@ -1,32 +1,36 @@ 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/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= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -36,18 +40,32 @@ 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.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/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-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-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= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -60,12 +78,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= @@ -102,24 +120,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= @@ -128,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= @@ -139,8 +163,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 +175,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,30 +186,37 @@ 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/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= @@ -203,39 +233,55 @@ 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/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= -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 +291,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 +304,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,15 +352,19 @@ 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= -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= @@ -324,8 +374,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= @@ -335,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= @@ -360,19 +408,16 @@ 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/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= @@ -383,14 +428,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 +440,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..fd2f08d8a 100644 --- a/cron/main.go +++ b/cron/main.go @@ -1,20 +1,18 @@ +// 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 (--capabilities.proxy-url). What is left here is the capability +// itself; everything else - flags, observability, serving, announcing - is Run. package main import ( - "github.com/smartcontractkit/capabilities/cron/trigger" - "github.com/smartcontractkit/capabilities/libs/loopserver" + "context" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron/server" - "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/capabilities/cron/trigger" + "github.com/smartcontractkit/capabilities/libs/capability" ) func main() { - loopserver.ServeNew(trigger.ServiceName, func(s *loop.Server) loop.StandardCapabilities { - triggerService, err := trigger.NewTriggerService(s.Logger, nil, s.LimitsFactory) - if err != nil { - s.Logger.Fatalw("Failed to create cron trigger service", "error", err) - } - - return server.NewCronServer(triggerService) - }, loop.WithOtelViews(trigger.MetricViews())) + capability.Run(context.Background(), trigger.NewCron, capability.WithOtelViews(trigger.MetricViews()...)) } diff --git a/cron/project.json b/cron/project.json index bcd75466f..09965f29e 100644 --- a/cron/project.json +++ b/cron/project.json @@ -1,7 +1,7 @@ { "name": "cron", "projectType": "capability", - "implicitDependencies": ["loopserver"], + "implicitDependencies": [], "tags": [], "targets": { "lint": { 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..4f31bab0c --- /dev/null +++ b/cron/protos/trigger_server_gen.go @@ -0,0 +1,150 @@ +// 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" + + "google.golang.org/protobuf/reflect/protoreflect" + + "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" + +// 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 { + 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/constructor.go b/cron/trigger/constructor.go new file mode 100644 index 000000000..1974f3729 --- /dev/null +++ b/cron/trigger/constructor.go @@ -0,0 +1,26 @@ +package trigger + +import ( + "github.com/smartcontractkit/capabilities/cron/protos" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" +) + +// NewCron is the constructor a cron binary hands to capability.Run. The parameter list is the +// declaration of what cron needs: the process's logger, its own config (bound by the host under +// the binary's name), and the limits the schedules it accepts are bounded by. Run reads that list +// and builds what it asks for. +// +// The clock is nil - the real one. nil is NewTriggerService's "a test drives this one". +// +// The generated server is what is returned rather than the service: it is the +// capability.Capability - servable, registerable, and carrying the proto service the requests are +// shaped by. +func NewCron(lggr logger.Logger, cfg Config, lf limits.Factory) (*protos.CronServer, error) { + triggerService, err := NewTriggerService(lggr, nil, cfg, Dependencies{LimitsFactory: lf}) + if err != nil { + return nil, err + } + return protos.NewCronServer(triggerService), nil +} diff --git a/cron/trigger/trigger.go b/cron/trigger/trigger.go index d78adf404..5a79bac05 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,12 @@ import ( "github.com/jonboulle/clockwork" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/smartcontractkit/capabilities/cron/protos" + "github.com/smartcontractkit/capabilities/libs/capability" + "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 +26,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 +35,36 @@ 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"` + // capability.Config marks this as the binary's own settings: the host binds them as flags + // under the binary's name, so the interval below is --cron.fastest-schedule-interval-seconds. + capability.Config + + 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 +93,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 +109,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 +121,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 +143,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 +180,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 +198,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 +211,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 +386,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 +396,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 +408,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()) } diff --git a/integration_tests/go.mod b/integration_tests/go.mod index 671a1d869..787c34eb5 100644 --- a/integration_tests/go.mod +++ b/integration_tests/go.mod @@ -25,11 +25,11 @@ require ( github.com/smartcontractkit/capabilities/http_action v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/capabilities/http_trigger v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/capabilities/loadtestwritetarget v0.0.0-00010101000000-000000000000 - github.com/smartcontractkit/chain-selectors v1.0.103 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260605180138-9678dea7f443 + github.com/smartcontractkit/chain-selectors v1.0.104 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260609161557-8ceae53b8ab1 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260604171908-6734db2d444f + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528221400-84746b70eeeb github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260609174137-e2407e0bdd98 github.com/smartcontractkit/cre-sdk-go v1.5.0 @@ -52,17 +52,17 @@ require ( 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.1.1 // 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/Azure/go-ntlmssp v0.1.1 // indirect - github.com/DataDog/zstd v1.5.6 // indirect + github.com/DataDog/zstd v1.5.7 // indirect github.com/Depado/ginprom v1.8.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/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 @@ -97,12 +97,12 @@ require ( github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect - github.com/cockroachdb/errors v1.11.3 // 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-20230118201751-21c54148d20b // 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/coder/websocket v1.8.14 // indirect github.com/cometbft/cometbft v0.38.21 // indirect github.com/cometbft/cometbft-db v1.0.1 // indirect @@ -147,7 +147,7 @@ require ( github.com/gagliardetto/solana-go v1.13.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 // indirect - github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/getsentry/sentry-go v0.35.1 // indirect github.com/gin-contrib/cors v1.7.2 // indirect github.com/gin-contrib/expvar v0.0.1 // indirect github.com/gin-contrib/sessions v0.0.5 // indirect @@ -265,24 +265,24 @@ require ( github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.2.0 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/olekukonko/tablewriter v1.0.9 // indirect github.com/onsi/gomega v1.38.2 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/peterldowns/pgtestdb v0.1.1 // indirect - github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/logging v0.2.2 // 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.1 // 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/pressly/goose/v3 v3.26.0 // 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 @@ -313,14 +313,14 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260415165642-49f23e4d76cc // indirect github.com/smartcontractkit/chainlink-ccv v0.0.2-0.20260428133800-3b1484e8b1fd // indirect - github.com/smartcontractkit/chainlink-common/keystore v1.2.0 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0 // 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-data-streams v0.1.15-0.20260522094612-5f9f748bd87a // indirect github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 // indirect github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243 // 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/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect @@ -329,18 +329,18 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260512230622-65f10f4cd305 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.1-0.20260528221400-84746b70eeeb // indirect github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect - github.com/smartcontractkit/chainlink-protos/svr v1.2.0 // indirect + github.com/smartcontractkit/chainlink-protos/svr v1.3.0 // indirect github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 // indirect github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260608211110-ed43ab034a6f // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a // 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-20260508200755-99940c85383c // indirect + github.com/smartcontractkit/libocr v0.0.0-20260529134643-c101335a64cd // indirect github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20241009055228-33d0c0bf38de // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20251120172354-e8ec0386b06c // indirect @@ -410,22 +410,22 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/arch v0.11.0 // 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/mod v0.36.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.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/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 gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // 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.81.0 // indirect + google.golang.org/grpc v1.82.1 // 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/integration_tests/go.sum b/integration_tests/go.sum index 74588f306..38bf6beed 100644 --- a/integration_tests/go.sum +++ b/integration_tests/go.sum @@ -62,6 +62,7 @@ filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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= @@ -84,6 +85,7 @@ 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.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Depado/ginprom v1.8.0 h1:zaaibRLNI1dMiiuj1MKzatm8qrcHzikMlCc1anqOdyo= github.com/Depado/ginprom v1.8.0/go.mod h1:XBaKzeNBqPF4vxJpNLincSQZeMDnZp1tIbU0FU0UKgg= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -93,8 +95,10 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo 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/juno v0.15.11/go.mod h1:DyfDC1vz8OpoAOWdGJif97Kueo4J7yhZUtYkkFUYg20= 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/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/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= @@ -235,16 +239,20 @@ github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaY 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/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-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +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/redact v1.1.6/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/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= @@ -412,6 +420,7 @@ github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 h1:Uc+IZ7gYqAf/rSG github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813/go.mod h1:P+oSoE9yhSRvsmYyZsshflcR6ePWYLql6UU1amW13IM= 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/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= @@ -963,6 +972,7 @@ github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -1009,6 +1019,7 @@ github.com/peterldowns/testy v0.0.1 h1:9a6LzvnKcL52Crzud1z7jbsAojTntCh89ho6mgsr4 github.com/peterldowns/testy v0.0.1/go.mod h1:J4sm75UEzbfBIcq0zbrshWWjsJQiJ5RrhTPYKVY2Ww8= 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/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= @@ -1018,6 +1029,7 @@ 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/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= @@ -1026,6 +1038,7 @@ 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.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +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= @@ -1042,6 +1055,7 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +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= @@ -1143,6 +1157,7 @@ github.com/smartcontractkit/capabilities/libs v0.0.0-20260609124022-2749e4a32bfb github.com/smartcontractkit/capabilities/libs v0.0.0-20260609124022-2749e4a32bfb/go.mod h1:LS7F8U2YZNc0Vt8f6SVWUUigGLxdxZMpyC7VCcUTagg= github.com/smartcontractkit/chain-selectors v1.0.103 h1:PpvIinn1TIDT7nh/P5KLQunRk0Kp1IR6moP2IGvlP58= github.com/smartcontractkit/chain-selectors v1.0.103/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= @@ -1161,10 +1176,13 @@ github.com/smartcontractkit/chainlink-ccv v0.0.2-0.20260428133800-3b1484e8b1fd h github.com/smartcontractkit/chainlink-ccv v0.0.2-0.20260428133800-3b1484e8b1fd/go.mod h1:SBN8Urnh5sQvrQRbSo1Nr8coWatHg8LZoPw3R/42sho= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260605180138-9678dea7f443 h1:uclvPWuit298UwfANmUUFWZdYX/pcQZLUosZVjwqs40= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260605180138-9678dea7f443/go.mod h1:fP9RqD25/gTx3XqRstN8o4lAI3jp42vwJBLRZwRoOOM= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260807193849-47d010760510/go.mod h1:DUnczFmJPvNHsQc9er8wdBvt4bPJzPOgr62i7mQ1jhM= 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/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= 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/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= github.com/smartcontractkit/chainlink-data-streams v0.1.15-0.20260522094612-5f9f748bd87a h1:8bIqv4r7SgDWkXL2Qz/Ijw+YjZY1uroIte3E2v2keVk= github.com/smartcontractkit/chainlink-data-streams v0.1.15-0.20260522094612-5f9f748bd87a/go.mod h1:dF5JiHWueHjYguUUUrFeb03MkcDqha/tssEkqTkgzp4= github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260609161557-8ceae53b8ab1 h1:VdJBtNmasHzISQQF0k0LHFh44WDKO7S00VyaT7qykuc= @@ -1179,8 +1197,10 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= +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-20260521164805-26d78d5e1243 h1:vaFBupfFfImQgqOeuC7Muk2GflbYP6Gpi0Y/TLroFU8= github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +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= @@ -1195,12 +1215,14 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260604171908-6734db2d444f h1:t+OoYaXLdH0WHK2pbWKjTSnSQa5JBQD1+gf0yISYfQk= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260604171908-6734db2d444f/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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/node-platform v0.0.0-20260512230622-65f10f4cd305 h1:bnSl5p3mFekSJ6QcbZ1TmHn2ffYiX8xk6hNzVmyhstQ= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260512230622-65f10f4cd305/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.1-0.20260528221400-84746b70eeeb h1:G8X3SR21VYAHWkDkNGZCjsrWrLJoVmXMpYBa2KKK3GU= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.1-0.20260528221400-84746b70eeeb/go.mod h1:m/A3lqD7ms/RsQ9BT5P2uceYY0QX5mIt4KQxT2G6qEo= github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd h1:7DURXB3+Qf9REr3XA+q0FNyZO3CSAeSgJvNaek/GiZI= @@ -1211,6 +1233,7 @@ github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 h1:B7itmjy+C github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0/go.mod h1:h6kqaGajbNRrezm56zhx03p0mVmmA2xxj7E/M4ytLUA= github.com/smartcontractkit/chainlink-protos/svr v1.2.0 h1:7jjgqRgORQS/ikL3z0ZgJy95pzjhR9LuU1TVWg4BZ78= github.com/smartcontractkit/chainlink-protos/svr v1.2.0/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= +github.com/smartcontractkit/chainlink-protos/svr v1.3.0/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528221400-84746b70eeeb h1:mlN8zK1UzDIBYtKSILQ4gci9MFwo42QFtGV1tWddMyk= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528221400-84746b70eeeb/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260605202330-b5a89c32fdc1 h1:e4vdi3czYy+eK2j/eO5r3ceMxxkx4Qq5IsiAeSAQ9uc= @@ -1237,6 +1260,7 @@ 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-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/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9 h1:MOEuXYogv+RStASb8dWsyescu/xkigSi/Sv45NEjV7A= github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9/go.mod h1:iwy4yWFuK+1JeoIRTaSOA9pl+8Kf//26zezxEXrAQEQ= github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb h1:kLHdQQkijaPGsBbtV2rJgpzVpQ96e7T10pzjNlWfK8U= @@ -1531,6 +1555,7 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf 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/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1632,6 +1657,7 @@ 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/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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1661,6 +1687,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ 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/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -1749,6 +1776,7 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.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/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= @@ -1762,6 +1790,7 @@ 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/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1779,6 +1808,7 @@ golang.org/x/text v0.13.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/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1959,6 +1989,7 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +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/libs/capability/AGENTS.md b/libs/capability/AGENTS.md new file mode 100644 index 000000000..db66f58b9 --- /dev/null +++ b/libs/capability/AGENTS.md @@ -0,0 +1,226 @@ +# `capability` — standalone capability binary, spike + +## What this is + +An in-progress replacement for `libs/standalone`'s bootstrapper. The goal is that a capability +binary's `main` is one line: + +```go +func main() { capability.Run(ctx, trigger.NewCron) } +``` + +Everything a hand-written `main` does today — registering flags, resolving dependencies, handing +them to a factory — is a restatement of the capability constructor's parameter list. `Run` reads +that list by reflection and builds what it asks for. + +The reference implementation to compare against is `libs/standalone/bootstrapper.go` (process +lifecycle) and `libs/standalone/capability` (capability hosting). Neither has been changed except +for one extraction (see *Relationship to `libs/standalone`*). + +`cron/main.go` is the first user: `capability.Run(ctx, trigger.NewCron, capability.WithOtelViews(...))`, +with `trigger.Config` embedding `capability.Config`. The old bootstrapper remains for everything +else. + +## Current state + +`RunErr(ctx, lggr, ctor, opts...)` builds a cobra root (named after the executable), registers the +config sections on it, and hangs `run` and `embed` subcommands off it. `embed` is a stub. + +`run` (in `runner.go`) is the whole sequence, in order: + +``` +profiler (nil when no pyroscope server configured) +telemetry building it installs the process-global beholder client +registry grpc.NewClient (lazy) + registry.Local[.WithRemote]; Add serves+announces +settings CRE settings from the dumped file, for the limits factory +startCapability build (newCapability.call) → Start → reg.Add (serve+announce) → debug UI if flagged +health checker reports on the services started before it +web server /metrics, /debug/pprof, /healthz, /readyz, /reload/settings.txt, + /debug/capabilities when --capabilities.http-debug +→ block: plugin.Serve under a go-plugin host, else <-ctx.Done() +→ defer MultiCloser(svcs): concurrent — close order is not controlled +``` + +`run` keeps a plain `[]services.Service`: each piece is **started as it is built** (`startX` +wrappers over `newX`) and appended, and a deferred `services.MultiCloser` at the top closes +whatever was appended — so a failure at any step unwinds everything before it. There is no +aggregate root service. + +Config is `config` (`config.go`) with `observability`, `capabilities` and `grpc` as siblings. Every +setting +is optional; `--http.port` defaults to 8080. A constructor may also declare one config struct of +its own by embedding `Config`; `bindConfig` registers it on the root under the binary's name +(`--cron.fastest-schedule-interval-seconds`, `CRE_CRON_...`) and `call` hands it over decoded. + +Dependency injection: `constructor.call` (`run.go`) matches constructor parameters to +`Dependencies` fields **by type, by assignability, one field at a time** — except the `Config` +parameter, which comes from the flags. Currently `Logger`, `CapabilityRegistry` and +`LimitsFactory`. + +## The list + +Numbered against `libs/standalone/bootstrapper.go` unless noted. + +### Done +1. Block until interrupted — `signal.NotifyContext` in `RunErr`, `<-ctx.Done()` in `run` +2. `plugin.Serve` under a go-plugin host — `underPluginHost()` + empty LOOP +3. `logger.Sync()` on shutdown — deferred first so it unwinds last +4. Logger named after the binary +6. `WithOtelViews` — `Option` on `Run`/`RunErr` +7. Start the capability — an entry in the run's service list +8. Health checker registration — registers its *siblings*, not the parent +16. Serve each capability on its own gRPC server — `server.go` + `registryService.Add`. + `server.go` is a copy of `libs/standalone/grpc` minus the configured single-server form + (crecore's), with one fix: the stop happens before the engine handoff, not in the `Close` + hook, because the engine waits for the `Serve` goroutine *before* running the hook — the old + arrangement deadlocks a started server's close. +17. Register and announce to the node's registry, remove on shutdown — `registryService.Add` and + `close`. The registry owns serving itself: `Add` binds the server, holds the value, and calls + `AddAt` with the address in scope, so the `addresses`-map convention `WithRemote` expects is + deliberately nil'd out. Announcing happens at build time (right after the constructor), so a + failed announce fails the run before it is nominally up and ready implies announced. `close` + is one ordered function — Remove, stop servers, close conn — and uses a fresh context, not the + engine's: `Close` closes the `StopChan` the engine's derives from before running the hook, so + the old `eng.NewCtx()` deregistration was cancelled before it left the process (latent in + `libs/standalone/capability` too). + +### Skipped +5. `commonConfig` — an empty struct upstream; not needed yet + +### Open — bootstrapper +9. Expose the run to constructors: the mux, metrics registerer, beholder client. The mux is the + run's own (created early in `run`, so the reload endpoint registers on it), but constructors + still have no way to reach it. +10. Resolve dependencies before calling the constructor — `bootstrap_gen.go` `Run1`…`Run10` +11. Close resolved dependencies in reverse — `registerCloser` (`:150`) +12. Register each dependency's config on the command that resolves it — + `setupCommands`/`collectTargets`/`configSet` (`:374-479`). Each config instance must be bound + exactly once or viper reads the wrong flag. + +### Open — embed +13. `embed` command + `--instances` (stub at `run.go`) +14. `ForEmbedding` — per-instance dependency forms +15. Per-instance identity: logger `instance.N`, prometheus `instance` label (`:342-348`), + `portFor(index)`, and distinct service names or health metrics collide between instances + +### Done — capability hosting +18. Settings reload endpoint — `reloadHandler` in `settings.go`, registered on the run's mux in + `run`. The node dumps the file and hits `/reload/settings.txt`; 200 means every limit now + resolves against the new payload, 500 means the previous settings are still in force. +19. Debug UI — `mountDebugUI` in `debugui.go`, under `/debug/capabilities`, gated on + `--capabilities.http-debug` (off by default: it invokes capabilities). Fleet and hub are one + each; they are the shared forms an embed run fans out over, so `embed` takes them over rather + than inventing its own. + +## Decisions, and the constraints behind them + +These are the non-obvious ones. Several were re-litigated more than once before the constraint was +found, so they are worth reading before changing the order of anything in `run`. + +**Telemetry is installed at build time, not in `Start`.** A capability creates its OTEL instruments +while it is being *constructed* (`cron/trigger/metrics.go:47` calls `beholder.GetMeter()` inside +`NewMetrics`), and an instrument resolves its meter once. Installing later leaves every capability +metric bound to the noop meter for the life of the process, silently. Same reason the limits factory +is built after telemetry. + +**The health checker cannot be inside the thing it reports on.** `Register` seeds state by calling +`reporter.Ready()` immediately and only re-reads on a **15s** tick (`services/health.go:63`). +Registering a still-starting aggregate would leave `/readyz` wrong for up to 15 seconds. It +registers its siblings instead — `run` hands it a snapshot of the slice built so far, which is +exactly what is already running when it starts. + +**Constructors ask for individual dependencies, never the `Dependencies` struct.** Taking the struct +would be a dependency on everything a run has, and adding a field would silently widen what every +capability appears to need. `offered()` enforces this by not offering the struct. + +**Matching is by assignability.** A capability can ask for `core.CapabilitiesRegistry` rather than +the wider `registry.Registry` the run holds. `provide()` reflects on the *dynamic* type, so a field +declared as a narrow interface still matches wider ones. + +**The registry serves what it registers.** `registryService.Add` binds the server, mounts the +capability, holds the value locally, and announces with `AddAt` — one function, so serve-first- +announce-last is control flow rather than convention, and the address never leaves the scope that +made it. The `addresses` map `WithRemote` announces from is nil'd out: a shared write between +whoever serves and whoever announces is exactly the coupling this removes. + +**The capability is started before it is announced.** `startCapability` runs the constructor, +starts the capability, and only then `reg.Add` serves and announces it — traffic the announcement +invites lands on something already running. Announcing at build time, rather than in a service's +`Start` of its own, means a failed announce fails the run before it is nominally up, and `/readyz` +implies announced. + +**`grpc.NewClient` does not dial, and does not validate.** The node starts this process and the two +race, so an eager dial would be a race we lose intermittently. It also accepts `""`, `"!://x"` and +unknown schemes without error — so a typo in `--capabilities.proxy-url` surfaces as a failed lookup +much later, not at startup. + +**Services start eagerly, in build order, and close concurrently** (`services/multi.go` — +`MultiCloser`). `run` starts each service as it builds it (`startX` wrappers) and appends it to +the slice; the deferred `MultiCloser` at the top of `run` closes whatever was appended, which is +what makes a failure at any step unwind everything before it. Close order is *not* controlled. + +**`StopOnce` refuses to run a `Close` hook on a service that never started** +(`services/state.go:111`, `ErrCannotStopUnstarted`). This is why anything that changes process state +at build time cannot rely on `Close` alone to undo it. + +## Known gaps and defects + +- **~~Telemetry global leaks on a pre-start failure~~ — resolved.** Every service is started as it + is built and appended to `svcs`, and the deferred `MultiCloser` at the top of `run` closes + whatever was appended, so a failure at any later step — the constructor, `newSettings`, + `reg.Add` — unwinds telemetry's global swap along with everything else. +- **~~A failure after `reg.Add` leaks the announcement~~ — resolved**, same mechanism: the + registry is started (so its `Close` runs) and on the slice before `startCapability` is called. +- **`embed` must not serve the plugin host.** `run` decides for itself via `underPluginHost()`, so + every instance would try. A host supervises one plugin. `TODO` on `run`. +- **Close ordering.** The registry's own close is now internally ordered (Remove → stop servers → + close conn), so the deregistration reliably reaches the node. What remains: the registry and the + capability are still closed concurrently (`MultiCloser`), so a draining RPC (servers stop with + `GracefulStop`) can call into a capability that is already closing. The bootstrapper closed + dependencies strictly after services (`registerCloser`). Unresolved. +- **Settings constants are a copy.** `settingsDirName`, `settingsFileName`, `reloadPathPrefix` in + `settings.go` duplicate `libs/standalone/capability/settings.go` because importing it would be a + cycle. They are a live contract with the node, which writes the file this reads. If they drift, + settings silently stop arriving and every limit falls back to its compiled-in default with no + error. `TestSettingsPathIsTheSharedConvention` guards this side only. +- **`CL_PROMETHEUS_PORT` is ignored.** Under a go-plugin host the node assigns a metrics port via + that env var, but this binds `CRE_HTTP_PORT`/`CL_HTTP_PORT`. Pre-existing — the bootstrapper has + the same naming — but now quieter, since the 8080 default means the binary starts anyway. +- **`loop.TracingConfig.OnDialError` is still nil in `libs/standalone/telemetry.go`.** Fixed here + (`telemetry.go`, `beholderConfig`); still live in the shipping bootstrapper, where + `--tracing.enabled` plus an unreachable collector is a nil dereference on a background goroutine + that kills the process. + +## Relationship to `libs/standalone` + +This package is a deliberate **copy** of the observability config and helpers, not a move. +`libs/standalone` imports this package for `NewLogger`, so this package cannot import it back +without a cycle. The duplicate is meant to resolve by *deletion* — when this is what starts a +binary, the ones in `standalone` go. + +The only change made to `libs/standalone` is that `newLogger` was moved out of `telemetry.go` into +this package's `logger.go`, and `bootstrapper.go` now calls `capability.NewLogger()`. + +## Running the tests + +``` +cd libs +go test ./capability/ -short # ~1s +go test ./capability/ -short -race +go test ./capability/ # ~21s +``` + +**Use `-short` by default.** One test — +`TestRunInstallsTelemetryBeforeBuildingTheCapability` — builds a real beholder client, and closing +one flushes to a collector that is not there: about 20s of export timeouts. It is gated on +`testing.Short()`. It is also the only test that would catch the silent-noop-metrics regression +described above, so do not delete it. + +Tests bind real TCP ports (`freePort`) and mutate `os.Args` and the beholder global, so they are not +parallel-safe. `execute()` in `run_test.go` drives the real entry point and stops the run by +cancelling its context once the HTTP server answers. + +## Style + +Comments explain *why*, not what — particularly the ordering constraints above, which are otherwise +invisible and have been reintroduced by accident more than once. Match the surrounding density. diff --git a/libs/capability/capability.go b/libs/capability/capability.go new file mode 100644 index 000000000..ab7cf27a4 --- /dev/null +++ b/libs/capability/capability.go @@ -0,0 +1,65 @@ +package capability + +import ( + "reflect" + + "google.golang.org/protobuf/reflect/protoreflect" + + "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/chainlink-common/pkg/types/core" +) + +type Capability interface { + services.Service + capabilities.ExecutableAndTriggerCapability + + // Service is the proto service this capability's server was generated from. + Service() protoreflect.ServiceDescriptor +} + +// Config marks the struct a constructor asks for as the capability's own configuration +// +// type Config struct { +// capability.Config +// FastestScheduleIntervalSeconds int `usage:"fastest cron schedule a workflow may register"` +// } +// +// The run binds the struct's fields as flags on the root command, namespaced by the binary's +// name - cron's are --cron.fastest-schedule-interval-seconds, CRE_CRON_FASTEST_SCHEDULE_INTERVAL_SECONDS, +// or the same key in the config file - and hands the constructor the decoded value. A constructor +// declares at most one. +type Config struct{} + +// capabilityConfig is what a capability binary needs from the node it runs beside. +type capabilityConfig struct { + ProxyURL string `usage:"gRPC target of the node's capability registry proxy (e.g. localhost:9000), used to resolve capabilities this binary does not host. Unset resolves only the capabilities this binary hosts"` + CapabilityDonID uint32 `usage:"on-chain DON ID of the capability DON this process was spawned for"` + + // Serves the Debug UI + HTTPDebug bool `usage:"serve the capability debug UI on the shared HTTP server, under /debug/capabilities"` +} + +type Dependencies struct { + Logger logger.Logger + CapabilityRegistry core.CapabilitiesRegistry + LimitsFactory limits.Factory +} + +func (d Dependencies) list() []any { + return []any{d.Logger, d.CapabilityRegistry, d.LimitsFactory} +} + +func (d Dependencies) resolve(want reflect.Type) (reflect.Value, bool) { + for _, v := range d.list() { + if v == nil { + continue + } + if got := reflect.TypeOf(v); got.AssignableTo(want) { + return reflect.ValueOf(v), true + } + } + return reflect.Value{}, false +} diff --git a/libs/capability/config.go b/libs/capability/config.go new file mode 100644 index 000000000..49490abfa --- /dev/null +++ b/libs/capability/config.go @@ -0,0 +1,48 @@ +package capability + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" +) + +const capabilitiesNamespace = "capabilities" + +const grpcNamespace = "grpc" + +// the standard configuration for a capability; does not include the config that a capability may request via its constructor. +type config struct { + observability observability + capabilities capabilityConfig + grpc grpcConfig +} + +func defaultConfig() *config { + return &config{ + observability: *defaultObservability(), + grpc: grpcConfig{AdvertiseHost: defaultHost}, + } +} + +// namespaced pairs every config with the namespace it is registered under, in the order the flags +// are registered. +func (c *config) namespaced() []section { + return append(c.observability.namespaced(), + section{capabilitiesNamespace, &c.capabilities}, + section{grpcNamespace, &c.grpc}, + ) +} + +// bind binds every config to root, each under the namespace that owns it. +func (c *config) bind(root *cobra.Command) error { + opts := flags.DefaultTOMLOptions("CRE", "CL") + for _, s := range c.namespaced() { + opts.Namespace = s.namespace + if err := flags.RegisterCommandFlags(root, s.target, opts); err != nil { + return fmt.Errorf("failed to register the %s settings: %w", s.namespace, err) + } + } + return nil +} diff --git a/libs/capability/debugui.go b/libs/capability/debugui.go new file mode 100644 index 000000000..bb2e57301 --- /dev/null +++ b/libs/capability/debugui.go @@ -0,0 +1,32 @@ +package capability + +import ( + "context" + "fmt" + "net/http" + + "github.com/smartcontractkit/capabilities/libs/standalone/protohelpers/ui" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// mountDebugUI serves the debug page for the capability this binary hosts. +func mountDebugUI(ctx context.Context, lggr logger.Logger, mux *http.ServeMux, registry ui.Registry, c Capability) error { + server, err := ui.New(ctx, registry, c) + if err != nil { + return fmt.Errorf("failed to build the capability debug UI: %w", err) + } + + if err := ui.Mount(ui.Options{ + Mux: mux, + Server: server, + Fleet: &ui.Fleet{}, + Hub: ui.NewHub(), + Title: "Capability debug", + }); err != nil { + return fmt.Errorf("failed to mount the capability debug UI: %w", err) + } + + lggr.Infow("Serving the capability debug UI", "path", ui.DefaultPrefix+"/ui/", "fanout", ui.DefaultPrefix+"/request") + return nil +} diff --git a/libs/capability/debugui_test.go b/libs/capability/debugui_test.go new file mode 100644 index 000000000..8d16ee49b --- /dev/null +++ b/libs/capability/debugui_test.go @@ -0,0 +1,57 @@ +package capability + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// TestRunServesTheDebugUIWhenAsked covers the opt-in: with --capabilities.http-debug the page is +// served under /debug/capabilities. +func TestRunServesTheDebugUIWhenAsked(t *testing.T) { + cfg, c, port := runArgs(t, newFake) + cfg.capabilities.HTTPDebug = true + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- run(ctx, logger.Test(t), "cron", cfg, c) }() + + require.Eventually(t, func() bool { return serving(int(port)) }, 30*time.Second, 5*time.Millisecond, + "the run should be serving while it is up") + + res, err := http.Get(fmt.Sprintf("http://localhost:%d/debug/capabilities/ui/", port)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusOK, res.StatusCode) + + cancel() + require.NoError(t, <-done) +} + +// TestRunServesNoDebugUIByDefault pins the default: the UI invokes capabilities, so a run that +// was not asked for it does not expose it. +func TestRunServesNoDebugUIByDefault(t *testing.T) { + cfg, c, port := runArgs(t, newFake) + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- run(ctx, logger.Test(t), "cron", cfg, c) }() + + require.Eventually(t, func() bool { return serving(int(port)) }, 30*time.Second, 5*time.Millisecond, + "the run should be serving while it is up") + + res, err := http.Get(fmt.Sprintf("http://localhost:%d/debug/capabilities/ui/", port)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + cancel() + require.NoError(t, <-done) +} diff --git a/libs/capability/logger.go b/libs/capability/logger.go new file mode 100644 index 000000000..c2e755b57 --- /dev/null +++ b/libs/capability/logger.go @@ -0,0 +1,22 @@ +package capability + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// 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") + }) +} diff --git a/libs/capability/observability.go b/libs/capability/observability.go new file mode 100644 index 000000000..26f704117 --- /dev/null +++ b/libs/capability/observability.go @@ -0,0 +1,46 @@ +package capability + +import "go.opentelemetry.io/otel/sdk/metric" + +const ( + telemetryNamespace = "telemetry" + tracingNamespace = "tracing" + chipIngressNamespace = "chip-ingress" + pyroscopeNamespace = "pyroscope" + httpNamespace = "http" +) + +// observability is every process-wide observability config, registered together and consumed once +// the command runs and they have been decoded. +type observability struct { + telemetry TelemetryConfig + tracing TracingConfig + chipIngress ChipIngressConfig + pyroscope PyroscopeConfig + http HTTPConfig + + otelViews []metric.View // supplied through capability.Run +} + +func defaultObservability() *observability { + return &observability{ + tracing: TracingConfig{SamplingRatio: 1}, + http: HTTPConfig{Port: defaultHTTPPort}, + } +} + +type section struct { + namespace string + target any +} + +// namespaced pairs each config with its namespace, in the order the flags are registered. +func (o *observability) namespaced() []section { + return []section{ + {telemetryNamespace, &o.telemetry}, + {tracingNamespace, &o.tracing}, + {chipIngressNamespace, &o.chipIngress}, + {pyroscopeNamespace, &o.pyroscope}, + {httpNamespace, &o.http}, + } +} diff --git a/libs/capability/observability_test.go b/libs/capability/observability_test.go new file mode 100644 index 000000000..57e3fc182 --- /dev/null +++ b/libs/capability/observability_test.go @@ -0,0 +1,98 @@ +package capability + +import ( + "fmt" + "io" + "net" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// TestProfilerWithoutAServer is the off case, which is every binary that has not configured +// pyroscope: there is no service at all, rather than one that does nothing. +// +// Nil rather than a no-op matters to the caller: the root would take a no-op and report on it, and +// a health report mentioning profiling that is not happening is worse than not mentioning it. +func TestProfilerWithoutAServer(t *testing.T) { + assert.Nil(t, newProfiler(logger.Test(t), "cron", PyroscopeConfig{})) +} + +// TestProfilerWithAServer covers the other side: a configured one is a service the root can take, +// named so it is tellable in a health report. +// +// It is built rather than started. Starting one dials a pyroscope server, which a test has no +// business standing up to prove a constructor returned something. +func TestProfilerWithAServer(t *testing.T) { + p := newProfiler(logger.Test(t), "cron", PyroscopeConfig{ServerAddress: "pyro:4040"}) + + require.NotNil(t, p) + assert.Equal(t, "Profiler", p.Name()) +} + +// TestWebServerServes is what the HTTP config buys: the endpoints an operator and a prometheus +// scrape depend on, on the configured port, and gone again when it stops. +func TestWebServerServes(t *testing.T) { + health, err := newHealthChecker(logger.Test(t), beholder.NewNoopClient(), []services.HealthReporter{newFake()}) + require.NoError(t, err) + require.NoError(t, health.Start(t.Context())) + t.Cleanup(func() { assert.NoError(t, health.Close()) }) + checker := health.checker + + port := freePort(t) + mux := http.NewServeMux() + + // A route registered before the server is built, which is when a service registers its own. + mux.HandleFunc("/capability", func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprintln(w, "hello") + }) + + web := newWebServer(logger.Test(t), HTTPConfig{Port: uint16(port)}, mux, checker) + require.NoError(t, web.Start(t.Context())) + + for path, want := range map[string]int{ + "/metrics": http.StatusOK, + "/debug/pprof/": http.StatusOK, + "/capability": http.StatusOK, + // The capability is built but not started, so it is not ready - which is the honest + // answer rather than a health check that reports on nothing. + "/readyz": http.StatusServiceUnavailable, + } { + res, err := http.Get(fmt.Sprintf("http://localhost:%d%s", port, path)) + require.NoError(t, err, path) + _, _ = io.Copy(io.Discard, res.Body) + require.NoError(t, res.Body.Close()) + assert.Equal(t, want, res.StatusCode, path) + } + + // Stopping frees the port, so the next thing to want it can have it. + require.NoError(t, web.Close()) + _, err = http.Get(fmt.Sprintf("http://localhost:%d/metrics", port)) + assert.Error(t, err, "the server should not answer once it has stopped") +} + +// TestWebServerReportsAPortItCannotHave covers the failure a misconfigured port gives: it is +// reported where it happens rather than as a server that silently never listens. +func TestWebServerReportsAPortItCannotHave(t *testing.T) { + health, err := newHealthChecker(logger.Test(t), beholder.NewNoopClient(), []services.HealthReporter{newFake()}) + require.NoError(t, err) + require.NoError(t, health.Start(t.Context())) + t.Cleanup(func() { assert.NoError(t, health.Close()) }) + checker := health.checker + + taken, err := net.Listen("tcp", ":0") + require.NoError(t, err) + t.Cleanup(func() { _ = taken.Close() }) + + // Building it registers routes and binds nothing, so the failure is at start. + port := uint16(taken.Addr().(*net.TCPAddr).Port) + web := newWebServer(logger.Test(t), HTTPConfig{Port: port}, http.NewServeMux(), checker) + + require.ErrorContains(t, web.Start(t.Context()), fmt.Sprintf("failed to listen on port %d", port)) +} diff --git a/libs/capability/pyroscope.go b/libs/capability/pyroscope.go new file mode 100644 index 000000000..e4f63074f --- /dev/null +++ b/libs/capability/pyroscope.go @@ -0,0 +1,93 @@ +package capability + +import ( + "context" + "fmt" + "runtime/debug" + + "github.com/grafana/pyroscope-go" + + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// 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 commonconfig.SecretString `usage:"pyroscope auth token" flagdocs:"noexample"` + Environment string `usage:"environment tag attached to profiles"` +} + +// newProfiler returns continuous profiling as a service, or nil when no pyroscope server is +// configured. +func newProfiler(lggr logger.Logger, appName string, cfg PyroscopeConfig) *profilerService { + if cfg.ServerAddress == "" { + return nil + } + + p := &profilerService{appName: appName, cfg: cfg} + p.Service, _ = services.Config{ + Name: "Profiler", + Start: p.start, + Close: p.close, + }.NewServiceEngine(lggr) + return p +} + +func startProfiler(ctx context.Context, lggr logger.Logger, appName string, cfg PyroscopeConfig) (*profilerService, error) { + profiler := newProfiler(lggr, appName, cfg) + if profiler == nil { + return nil, nil + } + + return profiler, profiler.Start(ctx) +} + +type profilerService struct { + services.Service + + appName string + cfg PyroscopeConfig + + // profiler is what start made and close stops. Written by one hook and read by the other, which + // the state machine's lock orders: a service cannot be closed unless it started. + profiler *pyroscope.Profiler +} + +func (p *profilerService) start(context.Context) error { + 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] + } + } + + profiler, err := pyroscope.Start(pyroscope.Config{ + ApplicationName: p.appName, + ServerAddress: p.cfg.ServerAddress, + AuthToken: string(p.cfg.AuthToken), + Tags: map[string]string{ + "version": ver, + "sha": sha, + "environment": p.cfg.Environment, + }, + ProfileTypes: []pyroscope.ProfileType{ + pyroscope.ProfileCPU, + pyroscope.ProfileAllocObjects, + pyroscope.ProfileAllocSpace, + pyroscope.ProfileInuseObjects, + pyroscope.ProfileInuseSpace, + }, + }) + if err != nil { + return fmt.Errorf("failed to start profiler: %w", err) + } + + p.profiler = profiler + return nil +} + +func (p *profilerService) close() error { return p.profiler.Stop() } diff --git a/libs/capability/registry.go b/libs/capability/registry.go new file mode 100644 index 000000000..8bd0979f7 --- /dev/null +++ b/libs/capability/registry.go @@ -0,0 +1,190 @@ +package capability + +import ( + "context" + "errors" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// newRegistry builds the registry this process holds its capabilities in and resolves others +// through, as a service. +// +// Nothing is dialled here. grpc.NewClient connects on the first RPC rather than when it is made, so +// a proxy that is not up yet delays the first lookup instead of failing the whole boot - which +// matters because the node starts this process and the two race. +// +// It does not validate the target either: an empty string and an unknown scheme both build a client +// happily, so the error below is close to unreachable and a typo shows up as a failed lookup rather +// than at startup. What catches an unset URL is the `validate:"required"` tag on the setting. +// +// It is built rather than started for the same reason telemetry is: a capability is handed the +// registry when it is constructed, which is before the root that owns this can start anything. +func newRegistry(lggr logger.Logger, cfg capabilityConfig, servers *serverFactory) (*registryService, error) { + r := ®istryService{servers: servers} + local := registry.Local(lggr) + + if cfg.ProxyURL == "" { + // No node to ask. The local registry is the whole of this process's: it resolves what this + // binary registered and nothing else, and the metadata calls - which DONs exist, what OCR + // configuration a capability runs under - fail rather than answering with something + // invented. That is what a binary run on its own wants, and it dials nothing. + r.proxy = local + } else { + conn, err := grpc.NewClient(cfg.ProxyURL, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("failed to create registry proxy client for %s: %w", cfg.ProxyURL, err) + } + r.conn = 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. + // + // The addresses map WithRemote takes is deliberately nil. Add on the result would announce + // the address it found written there - a convention shared between whoever serves a + // capability and whoever announces it. This service announces with AddAt instead, at the + // point the address exists, so the map would be a write nobody reads. + r.proxy = local.WithRemote(conn, nil) + } + + r.Service, r.eng = services.Config{Name: "CapabilityRegistry", Close: r.close}.NewServiceEngine(lggr) + return r, nil +} + +// startRegistry builds the registry and starts it. +func startRegistry(ctx context.Context, lggr logger.Logger, cfg capabilityConfig, servers *serverFactory) (*registryService, error) { + r, err := newRegistry(lggr, cfg, servers) + if err != nil { + return nil, err + } + return r, r.Start(ctx) +} + +// registryService is the registry this process holds its own capabilities in and resolves others +// through, the servers its own are reached on, and the connection to the node's registry - as one +// service, so that closing it undoes all three in order. +// +// There is no Start. A lazily-connected client has nothing to start, and the work with ordering in +// it - serving and announcing a capability - is Add's, called once the capability exists rather +// than when the root starts. +type registryService struct { + services.Service + eng *services.Engine + + // proxy resolves capabilities: what this binary hosts first, as the values it already holds, + // and the rest from the node behind conn. Local first is not an optimisation: a capability + // hosted here is a value this process already has, so resolving it locally hands back the + // implementation rather than a gRPC client looping back into this same process. + proxy registry.Registry + + // servers makes the gRPC server each Add serves its capability on - one per capability, since + // a registry addresses a capability by the address serving it. + servers *serverFactory + + // hosted is what Add made reachable, so close undoes exactly that, in reverse. + hosted []hosted + + // conn is the connection to the node's registry proxy, which resolutions, announcements and + // OCR questions all share. Nil when no proxy is configured, which is a binary with no node + // behind it. + conn *grpc.ClientConn +} + +// hosted is one capability being served, and the server serving it. +type hosted struct { + id string + server *server +} + +// Add makes c reachable: served on a gRPC server of its own, held in this process's registry, and +// announced to the node's. +// +// The order is the registration protocol. Serving comes first because the announcement is what +// invites traffic, so nothing is announced until it can be answered - and the address is announced +// where it is made, rather than written somewhere an Add can find it later, so the two cannot +// disagree. +// +// It is called at build time rather than from a Start of this service's own: a capability that +// cannot be announced fails the run before it is nominally up, and the health checker - which the +// run starts after this - only reports ready once this has run. The capability itself is already +// started by the caller, so traffic the announcement invites lands on something running. +func (r *registryService) Add(ctx context.Context, c Capability) error { + info, err := c.Info(ctx) + if err != nil { + return fmt.Errorf("failed to read the capability's info: %w", err) + } + + server, err := r.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) + } + // Undone here on any failure below rather than by close: a failure before the run starts this + // service means it never started, and StopOnce would refuse to run close's undo at all. + if err := registry.RegisterCapability(r.eng, server.grpcServer(), c, info.CapabilityType); err != nil { + _ = server.Close() + return fmt.Errorf("failed to serve capability %s: %w", info.ID, err) + } + if err := server.Start(ctx); err != nil { + _ = server.Close() + return fmt.Errorf("failed to start the server for capability %s: %w", info.ID, err) + } + + if err := r.proxy.Add(ctx, c); err != nil { + _ = server.Close() + return fmt.Errorf("failed to register capability %s: %w", info.ID, err) + } + + // Announced last: the announcement is what invites traffic, so nothing is announced until it + // can be served. With no node behind this process there is nothing to announce to. + if r.conn != nil { + if err := r.proxy.AddAt(ctx, info.ID, info.CapabilityType, server.address()); err != nil { + _ = r.proxy.Remove(ctx, info.ID) + _ = server.Close() + return fmt.Errorf("failed to announce capability %s: %w", info.ID, err) + } + } + + r.hosted = append(r.hosted, hosted{id: info.ID, server: server}) + r.eng.Infow("Registered capability", "capabilityID", info.ID, "type", info.CapabilityType, "address", server.address()) + return nil +} + +// close undoes every Add in reverse: stop inviting traffic (Remove drops the local hold and tells +// the node's registry to drop the address), then stop answering it, then release the connection. +// +// One ordered function rather than concurrent closes, because the steps are each other's +// preconditions: the Remove RPC has to reach the node before the connection it travels on closes. +// +// Failure to deregister is 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 *registryService) close() error { + // A context of its own rather than the engine's: Close closes the StopChan the engine's + // derives from before it runs this hook, so the deregistration would be cancelled before it + // left the process. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + for i := len(r.hosted) - 1; i >= 0; i-- { + h := r.hosted[i] + if err := r.proxy.Remove(ctx, h.id); err != nil { + r.eng.Warnw("Failed to deregister capability", "capabilityID", h.id, "err", err) + } + r.eng.ErrorIfFn(h.server.Close, "Failed to stop the server for capability "+h.id) + } + + // The registry first: what it closes is what it dialled of its own accord - the capability + // addresses it resolved - which are connections of their own rather than this one. + err := r.proxy.Close() + if r.conn == nil { + return err + } + return errors.Join(err, r.conn.Close()) +} diff --git a/libs/capability/registry_test.go b/libs/capability/registry_test.go new file mode 100644 index 000000000..2db7d3365 --- /dev/null +++ b/libs/capability/registry_test.go @@ -0,0 +1,172 @@ +package capability + +import ( + "context" + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" + + registrypb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry/pb" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// TestNewRegistryDoesNotDial is the property the boot sequence rests on: the node starts this +// process and the two race, so a registry proxy that is not up yet has to delay the first lookup +// rather than fail the run. +// +// Nothing is listening on the port below, and building the registry still succeeds. +func TestNewRegistryDoesNotDial(t *testing.T) { + reg, err := newRegistry(logger.Test(t), capabilityConfig{ProxyURL: "localhost:1", CapabilityDonID: 1}, + &serverFactory{host: defaultHost}) + + require.NoError(t, err, "an unreachable proxy should not fail construction") + require.NotNil(t, reg.proxy) + assert.Empty(t, reg.hosted, "nothing is served yet, so nothing is announced") + + require.NoError(t, reg.Start(t.Context())) + require.NoError(t, reg.Close()) +} + +// TestNewRegistryDoesNotValidateTheTarget pins something worth knowing before debugging one: not +// even a nonsense target fails here. +// +// grpc.NewClient defers everything to the first RPC, so a typo in --capabilities.proxy-url is not +// reported at startup - it surfaces as a failed capability lookup later on. What catches an unset +// one is the `validate:"required"` tag on the setting, not this. +func TestNewRegistryDoesNotValidateTheTarget(t *testing.T) { + for _, target := range []string{"", "!://not a target", "unknownscheme:///x"} { + t.Run(target, func(t *testing.T) { + reg, err := newRegistry(logger.Test(t), capabilityConfig{ProxyURL: target}, &serverFactory{host: defaultHost}) + require.NoError(t, err) + require.NoError(t, reg.Start(t.Context())) + assert.NoError(t, reg.Close()) + }) + } +} + +// TestNewRegistryWithoutAProxyIsLocalOnly covers a binary with no node behind it: it dials nothing, +// and resolves only what it hosts. +// +// The metadata calls failing is the point rather than a shortcoming. A process holding capability +// values has no way to know which DONs exist, so saying so beats answering with something invented. +func TestNewRegistryWithoutAProxyIsLocalOnly(t *testing.T) { + reg, err := newRegistry(logger.Test(t), capabilityConfig{CapabilityDonID: 1}, &serverFactory{host: defaultHost}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, reg.Close()) }) + require.NoError(t, reg.Start(t.Context())) + + assert.Nil(t, reg.conn, "nothing should have been dialled") + require.NotNil(t, reg.proxy) + + _, err = reg.proxy.DONByID(t.Context(), 1) + assert.Error(t, err, "there is no node to ask, so the metadata calls should fail") +} + +// TestRegistryAddServesAndHolds covers the local half of being reachable: the capability is served +// on an address of its own, and this process's registry resolves it. +func TestRegistryAddServesAndHolds(t *testing.T) { + reg, err := newRegistry(logger.Test(t), capabilityConfig{}, &serverFactory{host: defaultHost}) + require.NoError(t, err) + require.NoError(t, reg.Start(t.Context())) + + require.NoError(t, reg.Add(t.Context(), newFake())) + + // Served: the hosted server's address answers. + require.Len(t, reg.hosted, 1) + conn, err := net.DialTimeout("tcp", reg.hosted[0].server.address(), 5*time.Second) + require.NoError(t, err) + _ = conn.Close() + + // Held: the registry resolves the capability as a value. + got, err := reg.proxy.Get(t.Context(), fakeID) + require.NoError(t, err) + assert.NotNil(t, got) + + // Close takes both back. Remove hollows the registry's entry out rather than deleting it, so + // what the ID still maps to answers nothing. + require.NoError(t, reg.Close()) + + got, err = reg.proxy.Get(t.Context(), fakeID) + if err == nil { + _, err = got.Info(t.Context()) + } + require.Error(t, err, "the registry should no longer hold the capability") +} + +// TestRegistryAddAnnouncesToTheNode covers the remote half: with a proxy configured, adding the +// capability announces the address it is served at, and closing takes the announcement back before +// the connection it travelled on closes. +func TestRegistryAddAnnouncesToTheNode(t *testing.T) { + stub := &stubRegistry{adds: map[string]string{}} + + reg, err := newRegistry(logger.Test(t), capabilityConfig{ProxyURL: serveStubRegistry(t, stub)}, + &serverFactory{host: defaultHost}) + require.NoError(t, err) + require.NoError(t, reg.Start(t.Context())) + + require.NoError(t, reg.Add(t.Context(), newFake())) + + // Announced at the address it is served at, and that address answers. + require.Len(t, reg.hosted, 1) + addr, ok := stub.announced(fakeID) + require.True(t, ok, "the node's registry should know the capability") + assert.Equal(t, reg.hosted[0].server.address(), addr) + conn, err := net.DialTimeout("tcp", addr, 5*time.Second) + require.NoError(t, err) + _ = conn.Close() + + require.NoError(t, reg.Close()) + stub.mu.Lock() + defer stub.mu.Unlock() + assert.Contains(t, stub.removes, fakeID, "shutdown should take the announcement back") +} + +// stubRegistry is the node's registry, minimally: it records what is announced to it, and what is +// taken back. +type stubRegistry struct { + registrypb.UnimplementedCapabilitiesRegistryServer + + mu sync.Mutex + adds map[string]string // capability ID -> the address it was announced at + removes []string +} + +func (s *stubRegistry) Add(_ context.Context, req *registrypb.AddRequest) (*emptypb.Empty, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.adds[req.CapabilityId] = req.CallbackUrl + return &emptypb.Empty{}, nil +} + +func (s *stubRegistry) Remove(_ context.Context, req *registrypb.RemoveRequest) (*emptypb.Empty, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.removes = append(s.removes, req.CapabilityId) + return &emptypb.Empty{}, nil +} + +func (s *stubRegistry) announced(id string) (string, bool) { + s.mu.Lock() + defer s.mu.Unlock() + addr, ok := s.adds[id] + return addr, ok +} + +// serveStubRegistry runs a stub registry on a free port and returns its address. +func serveStubRegistry(t *testing.T, stub *stubRegistry) string { + t.Helper() + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + gsrv := grpc.NewServer() + registrypb.RegisterCapabilitiesRegistryServer(gsrv, stub) + go func() { _ = gsrv.Serve(listener) }() + t.Cleanup(gsrv.Stop) + return listener.Addr().String() +} diff --git a/libs/capability/run.go b/libs/capability/run.go new file mode 100644 index 000000000..f89f52fd3 --- /dev/null +++ b/libs/capability/run.go @@ -0,0 +1,225 @@ +package capability + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/signal" + "path/filepath" + "reflect" + "syscall" + + "github.com/spf13/cobra" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/config/flags" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// Run builds and runs the capability constructor makes, and does not return until ctx is cancelled or the +// process is signalled. +// +// func main() { capability.Run(context.Background(), trigger.NewCron) } +func Run(ctx context.Context, constructor any, opts ...Option) { + lggr, err := NewLogger() + if err != nil { + log.Fatal(err) + } + + if err := RunErr(ctx, lggr, constructor, opts...); err != nil { + lggr.Fatal(err) + } +} + +// RunErr is Run, returning the error rather than exiting on it. +// +// A run blocks until it is told to stop, which is either ctx being cancelled or this process being +// signalled - see below. +func RunErr(ctx context.Context, lggr logger.Logger, constructor any, opts ...Option) error { + if lggr == nil { + return errors.New("must provide a logger") + } + + // We instantiate this up-front so we can bubble up an error message about an invalid `constructor` early. + c, err := newConstructor(constructor) + if err != nil { + return err + } + + // TODO: Run should accept some kind of Info struct + root := &cobra.Command{ + Use: filepath.Base(os.Args[0]), + Short: "A CRE capability", + } + root.PersistentFlags().String("config", "", "Path to config file") + + lggr = logger.Named(lggr, root.Name()) + + cfg := defaultConfig() + for _, opt := range opts { + opt(cfg) + } + if err := cfg.bind(root); err != nil { + return err + } + + // Bind the capability's config if the constructor function declares a config struct. + // This will be namespaced using the name of the root command. + // eg. --cron.fastest-schedule-interval-seconds + if err := c.bindCapabilityConfig(root); err != nil { + return err + } + + root.AddCommand(&cobra.Command{ + Use: "run", + Short: "Run the capability", + RunE: func(cmd *cobra.Command, _ []string) error { + // TODO: pass in constructors here for our runnable/embeddable dependencies? + return run(cmd.Context(), lggr, cmd.Root().Name(), cfg, c) + }, + }) + + // TODO: embed command + root.AddCommand(&cobra.Command{ + Use: "embed", + Short: "Embed the capability", + RunE: func(cmd *cobra.Command, _ []string) error { + // TODO: pass in constructors here for our runnable/embeddable dependencies? + // Something else here + return run(cmd.Context(), lggr, cmd.Root().Name(), cfg, c) + }, + }) + + // Wire up SIGTERM + SIGINT to the context + ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer stop() + + return root.ExecuteContext(ctx) +} + +type constructor struct { + fn reflect.Value + typ reflect.Type + + // configIn is the index of the parameter that is the capability's own config, or -1 when the + // constructor declares none. + configIn int + // config is the instance of that parameter the flags decode fills. Kept here rather than + // passed to run because the decode hook fills it in place when the command runs: call has to + // read that same memory, not a copy made when it was bound. Valid only when configIn >= 0. + config reflect.Value +} + +var capabilityType = reflect.TypeFor[Capability]() + +// newConstructor validates that constructor has the right shape, i.e.: +// - it's a function +// - and the function returns a capability and optionally an error +func newConstructor(ctor any) (constructor, error) { + t := reflect.TypeOf(ctor) + if t == nil || t.Kind() != reflect.Func { + return constructor{}, fmt.Errorf("a capability constructor must be a function, got %T", ctor) + } + + switch t.NumOut() { + case 1: + case 2: + if t.Out(1) != reflect.TypeFor[error]() { + return constructor{}, fmt.Errorf( + "a capability constructor returning two values must return an error second, got %s", t) + } + default: + return constructor{}, fmt.Errorf( + "a capability constructor must return the capability, and optionally an error, got %s", t) + } + + if out := t.Out(0); !out.Implements(capabilityType) { + return constructor{}, fmt.Errorf("%s does not implement capability.Capability", out) + } + + configIn := -1 + for i := range t.NumIn() { + if !isConfig(t.In(i)) { + continue + } + if configIn >= 0 { + return constructor{}, fmt.Errorf("a capability constructor takes its config once, got %s", t) + } + configIn = i + } + return constructor{fn: reflect.ValueOf(ctor), typ: t, configIn: configIn}, nil +} + +var isConfigType = reflect.TypeFor[Config]() + +// isConfig reports whether t declares itself a capability's config by embedding Config. The +// embedding has to be direct: the marker declares the struct it is embedded in, not the structs +// that embed that one. +func isConfig(t reflect.Type) bool { + if t.Kind() != reflect.Struct { + return false + } + for i := range t.NumField() { + if f := t.Field(i); f.Anonymous && f.Type == isConfigType { + return true + } + } + return false +} + +// bindCapabilityConfig creates the config the constructor declares, if any, and binds its fields as flags +// on root, under root's name. +func (c *constructor) bindCapabilityConfig(root *cobra.Command) error { + if c.configIn < 0 { + return nil + } + + v := reflect.New(c.typ.In(c.configIn)) + fopts := flags.DefaultTOMLOptions("CRE", "CL") + fopts.Namespace = root.Name() + if err := flags.RegisterCommandFlags(root, v.Interface(), fopts); err != nil { + return fmt.Errorf("failed to register the capability's config: %w", err) + } + c.config = v.Elem() + return nil +} + +// call builds the capability by calling the contstructor. +// The constructor's parameters are scanned, and type matched against the dependency set. +// The config parameter is treated exceptionally and hydrated from the configuration that was passed in. +func (c constructor) call(deps Dependencies) (Capability, error) { + args := make([]reflect.Value, c.typ.NumIn()) + for i := range args { + if i == c.configIn { + args[i] = c.config + continue + } + + want := c.typ.In(i) + + v, ok := deps.resolve(want) + if !ok { + return nil, fmt.Errorf("the capability constructor asks for a %s, and nothing in this run "+ + "provides one: %s", want, c.typ) + } + args[i] = v + } + + out := c.fn.Call(args) + if len(out) == 2 && !out[1].IsNil() { + return nil, fmt.Errorf("failed to build the capability: %w", out[1].Interface().(error)) + } + return out[0].Interface().(Capability), nil +} + +type Option func(*config) + +// WithOtelViews sets otel metric views - histogram bucket boundaries, typically - on the beholder +// client this process reports through. +// +// capability.Run(ctx, trigger.NewCron, capability.WithOtelViews(trigger.MetricViews()...)) +func WithOtelViews(views ...sdkmetric.View) Option { + return func(c *config) { c.observability.otelViews = append(c.observability.otelViews, views...) } +} diff --git a/libs/capability/run_test.go b/libs/capability/run_test.go new file mode 100644 index 000000000..64b5245ea --- /dev/null +++ b/libs/capability/run_test.go @@ -0,0 +1,386 @@ +package capability + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "os" + "slices" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" + "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/chainlink-common/pkg/types/core" +) + +// fake stands in for a generated capability server, which is what a real constructor returns. Only +// its type matters here: nothing calls it, because building it is as far as this step goes. +// +// The interfaces are embedded rather than implemented so that this says what a capability is - a +// service, a capability, and the proto service behind it - without a page of methods saying it. +type fake struct { + runnable + capabilities.ExecutableAndTriggerCapability + + // info is what the fake reports itself as, which is what the registrar serves and announces + // it by. + info capabilities.CapabilityInfo + + // started and closed record what the root service did with it. Written on the run's goroutine + // and read on the test's, but only after the run has returned, so the channel that reports that + // is what orders them. + started bool + closed bool +} + +// runnable holds the services.Service half. It is a type of its own so that the field it embeds is +// not called Service, which would collide with the method below. +type runnable struct{ services.Service } + +// Service is the base capability's descriptor rather than nil: the debug UI builds its page from +// what this returns and refuses a capability without one. +func (fake) Service() protoreflect.ServiceDescriptor { + return capabilitiespb.File_capabilities_proto.Services().ByName("BaseCapability") +} + +func (f *fake) Info(context.Context) (capabilities.CapabilityInfo, error) { return f.info, nil } + +// fakeID is what the fake registers and announces itself as. +const fakeID = "fake@1.0.0" + +// newFake builds one with a real service behind it, since the health checker asks the capability +// its name and whether it is ready. +func newFake() *fake { + f := &fake{} + f.info, _ = capabilities.NewCapabilityInfo(fakeID, capabilities.CapabilityTypeCombined, "a fake") + f.runnable.Service, _ = services.Config{ + Name: "FakeCapability", + Start: func(context.Context) error { f.started = true; return nil }, + Close: func() error { f.closed = true; return nil }, + }.NewServiceEngine(logger.Nop()) + return f +} + +// execute drives the real entry point with args of its own, and stops it once it is up. +// +// os.Args rather than a seam into the command tree, because that is what RunErr reads and what a +// binary is actually started with - including argv[0], which is what names the root command. +// +// A run blocks until its context is cancelled, so this waits until the binary is serving and then +// cancels - which is a test standing in for the signal an operator would send. A run that fails on +// the way up never gets there and reports that instead. +func execute(t *testing.T, lggr logger.Logger, ctor any, args ...string) error { + t.Helper() + + // A free port rather than the default, since tests share a machine and run alongside each + // other - two of them on 8080 would collide. It is also how this tells that the binary is up. + port := 0 + if slices.Contains(args, "run") && !slices.Contains(args, "--http.port") { + port = freePort(t) + args = append(args, "--http.port", strconv.Itoa(port)) + + // No --capabilities.proxy-url: it is optional, and leaving it out is the simpler run - the + // registry then resolves only what this binary hosts, and dials nothing. + } + + previous := os.Args + t.Cleanup(func() { os.Args = previous }) + os.Args = append([]string{"cron"}, args...) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := make(chan error, 1) + go func() { done <- RunErr(ctx, lggr, ctor) }() + + if port == 0 { + // Nothing that serves: a rejected constructor, or a bare invocation that prints help. + return <-done + } + + deadline := time.After(30 * time.Second) + for { + select { + case err := <-done: + return err + case <-deadline: + cancel() + t.Fatal("the run never started serving") + return nil + case <-time.After(5 * time.Millisecond): + if serving(port) { + cancel() + return <-done + } + } + } +} + +// serving reports whether the shared HTTP server is answering on port, which is how a test tells +// that a run has finished coming up. +// +// A timeout, because the port is not always the run's: a test that takes the port to watch the +// run fail leaves a listener that accepts and never answers, and a bare Get would hang on it +// rather than report not-serving. +func serving(port int) bool { + client := &http.Client{Timeout: 5 * time.Second} + res, err := client.Get(fmt.Sprintf("http://localhost:%d/metrics", port)) + if err != nil { + return false + } + _ = res.Body.Close() + return res.StatusCode == http.StatusOK +} + +// freePort is a port nothing is listening on, found by listening on one and stopping again. +func freePort(t *testing.T) int { + t.Helper() + + l, err := net.Listen("tcp", ":0") + require.NoError(t, err) + port := l.Addr().(*net.TCPAddr).Port + require.NoError(t, l.Close()) + return port +} + +func TestRunErrRejects(t *testing.T) { + lggr := logger.Test(t) + + t.Run("no logger", func(t *testing.T) { + require.ErrorContains(t, RunErr(context.Background(), nil, newFake), "must provide a logger") + }) + + t.Run("something that is not a function", func(t *testing.T) { + require.ErrorContains(t, RunErr(context.Background(), lggr, "cron"), "must be a function, got string") + }) + + t.Run("nothing at all", func(t *testing.T) { + require.ErrorContains(t, RunErr(context.Background(), lggr, nil), "must be a function") + }) + + t.Run("a function returning something that cannot be hosted", func(t *testing.T) { + require.ErrorContains(t, RunErr(context.Background(), lggr, func() string { return "" }), + "string does not implement capability.Capability") + }) + + t.Run("a function returning nothing", func(t *testing.T) { + require.ErrorContains(t, RunErr(context.Background(), lggr, func() {}), "must return the capability") + }) + + t.Run("a function whose second result is not an error", func(t *testing.T) { + require.ErrorContains(t, RunErr(context.Background(), lggr, func() (*fake, string) { return nil, "" }), + "must return an error second") + }) +} + +func TestRunBuildsTheCapability(t *testing.T) { + built := 0 + + require.NoError(t, execute(t, logger.Test(t), func() *fake { built++; return newFake() }, "run")) + assert.Equal(t, 1, built, "the constructor should have been called exactly once") +} + +func TestRunReportsAFailedConstructor(t *testing.T) { + err := execute(t, logger.Test(t), func() (*fake, error) { return nil, errors.New("no schedule") }, "run") + require.ErrorContains(t, err, "failed to build the capability: no schedule") +} + +// TestRunNamesWhatItCannotProvide covers a parameter nothing answers: it is reported before +// anything starts, and names the type that went unmatched rather than the position it was in. +func TestRunNamesWhatItCannotProvide(t *testing.T) { + err := execute(t, logger.Test(t), func(string) *fake { return newFake() }, "run") + require.ErrorContains(t, err, "asks for a string, and nothing in this run provides one") +} + +// TestRunPassesTheLoggerToTheConstructor covers the first dependency: this process's logger, so a +// capability names its own from it rather than building one the operator cannot configure. +func TestRunPassesTheLoggerToTheConstructor(t *testing.T) { + var got logger.Logger + + require.NoError(t, execute(t, logger.Test(t), func(l logger.Logger) *fake { + got = l + return newFake() + }, "run")) + + assert.NotNil(t, got, "the constructor should have been handed the run's logger") +} + +// TestRunPassesTheRegistryToTheConstructor covers the registry a capability resolves other +// capabilities through. +// +// By type rather than by position, so a constructor asks for what it needs and takes nothing it +// does not - which is what lets the one below coexist with the no-argument constructors elsewhere +// in these tests. +func TestRunPassesTheRegistryToTheConstructor(t *testing.T) { + var got registry.Registry + + require.NoError(t, execute(t, logger.Test(t), func(r registry.Registry) *fake { + got = r + return newFake() + }, "run")) + + assert.NotNil(t, got, "the constructor should have been handed the run's registry") +} + +// TestRunPassesTheLimitsFactoryToTheConstructor covers the second dependency: what a capability +// bounds a workflow's requests with. +// +// A struct rather than an interface, which the type matching handles the same way - what a +// constructor asks for is a type, not a kind of type. +func TestRunPassesTheLimitsFactoryToTheConstructor(t *testing.T) { + var got limits.Factory + + require.NoError(t, execute(t, logger.Test(t), func(f limits.Factory) *fake { + got = f + return newFake() + }, "run")) + + assert.NotNil(t, got.Settings, "the factory should be built over this run's settings") + assert.NotNil(t, got.Meter) +} + +// TestRunPassesEveryDependencyItHas covers a constructor asking for more than one, in an order of +// its own: matching is by type, so the order it lists them in is its business. +func TestRunPassesEveryDependencyItHas(t *testing.T) { + var ( + gotLimits limits.Factory + gotRegistry core.CapabilitiesRegistry + ) + + require.NoError(t, execute(t, logger.Test(t), func(f limits.Factory, r core.CapabilitiesRegistry) *fake { + gotLimits, gotRegistry = f, r + return newFake() + }, "run")) + + assert.NotNil(t, gotLimits.Settings) + assert.NotNil(t, gotRegistry) +} + +// testConfig is a capability's own config, declared by embedding Config: the one parameter an +// operator supplies rather than the run resolves. +type testConfig struct { + Config + Shout string `usage:"what the fake says"` +} + +// TestRunHandsTheConstructorItsConfig covers the parameter that is not a dependency: the +// capability's own settings, bound under the binary's name and handed over decoded. The binary is +// named cron - execute's argv[0] - so its flag is --cron.shout. +func TestRunHandsTheConstructorItsConfig(t *testing.T) { + var got testConfig + + require.NoError(t, execute(t, logger.Test(t), func(cfg testConfig) *fake { + got = cfg + return newFake() + }, "run", "--cron.shout", "hello")) + + assert.Equal(t, "hello", got.Shout) +} + +// TestRunReadsTheCapabilityConfigFromTheEnvironment is the same binding from the other direction: +// CRE_, then the binary's name, as every other section's settings are. +func TestRunReadsTheCapabilityConfigFromTheEnvironment(t *testing.T) { + t.Setenv("CRE_CRON_SHOUT", "from the environment") + + var got testConfig + + require.NoError(t, execute(t, logger.Test(t), func(cfg testConfig) *fake { + got = cfg + return newFake() + }, "run")) + + assert.Equal(t, "from the environment", got.Shout) +} + +// TestRunHandsTheConstructorItsConfigAndItsDependencies covers the two kinds of parameter side by +// side: the config from the flags, the dependencies from the run, and neither answered by the +// other. +func TestRunHandsTheConstructorItsConfigAndItsDependencies(t *testing.T) { + var ( + gotCfg testConfig + gotLimits limits.Factory + ) + + require.NoError(t, execute(t, logger.Test(t), func(cfg testConfig, f limits.Factory) *fake { + gotCfg, gotLimits = cfg, f + return newFake() + }, "run", "--cron.shout", "hello")) + + assert.Equal(t, "hello", gotCfg.Shout) + assert.NotNil(t, gotLimits.Settings) +} + +// TestRunRejectsTwoConfigs pins the one-config rule: the struct is bound under the binary's name, +// so two of them would share one namespace and which setting belonged to which would be anyone's +// guess. +func TestRunRejectsTwoConfigs(t *testing.T) { + err := execute(t, logger.Test(t), func(a, b testConfig) *fake { return newFake() }, "run") + require.ErrorContains(t, err, "takes its config once") +} + +// TestRunRejectsTheDependenciesStruct pins the rule the other way round: a constructor declares the +// things it uses, not the bag they came in. +// +// Taking the struct would be a dependency on everything a run has, and adding a field to it would +// silently widen what every capability appeared to need. +func TestRunRejectsTheDependenciesStruct(t *testing.T) { + err := execute(t, logger.Test(t), func(Dependencies) *fake { return newFake() }, "run") + + require.ErrorContains(t, err, "asks for a capability.Dependencies, and nothing in this run provides one") +} + +// TestRunPassesTheRegistryAsANarrowerInterface covers matching by assignability: a capability that +// only needs to resolve capabilities can say so, without naming the wider type the run holds. +func TestRunPassesTheRegistryAsANarrowerInterface(t *testing.T) { + var got core.CapabilitiesRegistry + + require.NoError(t, execute(t, logger.Test(t), func(r core.CapabilitiesRegistry) *fake { + got = r + return newFake() + }, "run")) + + assert.NotNil(t, got) +} + +// TestRunHasARunCommand is the shape of the binary rather than what it does: a root that runs +// nothing itself, and a way to start it hanging off it. +func TestRunHasARunCommand(t *testing.T) { + // A bare invocation prints help and does nothing, rather than starting the capability. + require.NoError(t, execute(t, logger.Test(t), func() *fake { + t.Error("the root command should not build the capability") + return newFake() + })) +} + +// TestRunServesAndAnnouncesTheCapability is the whole path through the real entry point: a run +// serves its capability, tells the node's registry where, and takes it back on shutdown. +// +// The removal half is reliable to assert here because the registry's close is one ordered +// function: the Remove RPC is sent before the connection it travels on is closed. +func TestRunServesAndAnnouncesTheCapability(t *testing.T) { + stub := &stubRegistry{adds: map[string]string{}} + proxyURL := serveStubRegistry(t, stub) + + require.NoError(t, execute(t, logger.Test(t), func() *fake { return newFake() }, + "run", "--capabilities.proxy-url", proxyURL)) + + addr, ok := stub.announced(fakeID) + require.True(t, ok, "the run should have announced the capability to the node's registry") + assert.NotEmpty(t, addr) + + stub.mu.Lock() + defer stub.mu.Unlock() + assert.Contains(t, stub.removes, fakeID, "and deregistered it on the way out") +} diff --git a/libs/capability/runner.go b/libs/capability/runner.go new file mode 100644 index 000000000..81523682d --- /dev/null +++ b/libs/capability/runner.go @@ -0,0 +1,139 @@ +package capability + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/hashicorp/go-plugin" + + "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" +) + +func run(ctx context.Context, lggr logger.Logger, name string, cfg *config, newCapability constructor) error { + defer func() { _ = lggr.Sync() }() + + var svcs []services.Service + defer func() { + if err := services.MultiCloser(svcs).Close(); err != nil { + logger.Sugared(lggr).Errorw("failed to stop the services of this run", "err", err) + } + }() + + mux := http.NewServeMux() + + profiler, err := startProfiler(ctx, lggr, name, cfg.observability.pyroscope) + if err != nil { + return fmt.Errorf("failed to start profiler: %w", err) + } + if profiler != nil { + svcs = append(svcs, profiler) + } + + telemetry, err := startTelemetry(ctx, lggr, &cfg.observability) + if err != nil { + return fmt.Errorf("failed to build telemetry: %w", err) + } + svcs = append(svcs, telemetry) + + reg, err := startRegistry(ctx, lggr, cfg.capabilities, &serverFactory{ + host: cfg.grpc.AdvertiseHost, + startPort: cfg.grpc.StartPort, + }) + if err != nil { + return fmt.Errorf("failed to start registry: %w", err) + } + svcs = append(svcs, reg) + + settings, err := newSettings(lggr) + if err != nil { + return err + } + mux.HandleFunc(reloadPath(), reloadHandler(lggr, settings, settingsPath())) + + capability, err := startCapability(ctx, lggr, cfg, newCapability, reg, settings, mux) + if err != nil { + return err + } + svcs = append(svcs, capability) + + health, err := startHealthChecker(ctx, lggr, beholder.GetClient(), servicesToHealthReporters(svcs)) + if err != nil { + return fmt.Errorf("failed to start health checker: %w", err) + } + svcs = append(svcs, health) + + ws, err := startWebServer(ctx, lggr, cfg.observability.http, mux, health.checker) + if err != nil { + return fmt.Errorf("failed to start web server: %w", err) + } + svcs = append(svcs, ws) + + if underPluginHost() { + lggr.Info("Serving the empty LOOP: this process is supervised by a go-plugin host") + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: loop.EmptyHandshakeConfig(), + Plugins: map[string]plugin.Plugin{loop.PluginEmptyName: &loop.EmptyLoop{}}, + GRPCServer: plugin.DefaultGRPCServer, + }) + return nil + } + + <-ctx.Done() + lggr.Info("Shutting down") + return nil +} + +func servicesToHealthReporters(svcs []services.Service) []services.HealthReporter { + reporters := make([]services.HealthReporter, 0, len(svcs)) + for _, s := range svcs { + reporters = append(reporters, s) + } + return reporters +} + +// underPluginHost reports whether this process was launched by a go-plugin host, detected via the +// empty plugin's handshake magic cookie. +// +// The check is necessary rather than defensive: go-plugin's Serve refuses to run - and exits the +// process - when the cookie is absent, so a standalone binary that called it would die on startup. +func underPluginHost() bool { + h := loop.EmptyHandshakeConfig() + return os.Getenv(h.MagicCookieKey) == h.MagicCookieValue +} + +// startCapability builds the capability from its constructor and makes it reachable: started, +// served and announced by the registry, and mounted via the debug UI if debug mode is turned on. +func startCapability(ctx context.Context, lggr logger.Logger, cfg *config, ctor constructor, reg *registryService, settings *loop.AtomicSettings, mux *http.ServeMux) (Capability, error) { + c, err := ctor.call(Dependencies{ + Logger: lggr, + CapabilityRegistry: reg.proxy, + LimitsFactory: newLimitsFactory(lggr, settings), + }) + if err != nil { + return nil, fmt.Errorf("failed to instantiate capability: %w", err) + } + + if err := c.Start(ctx); err != nil { + return nil, fmt.Errorf("failed to start capability: %w", err) + } + + if err := reg.Add(ctx, c); err != nil { + // Not yet on the caller's list, so the deferred close would never reach it. + _ = c.Close() + return nil, err + } + + if cfg.capabilities.HTTPDebug { + if err := mountDebugUI(ctx, lggr, mux, reg.proxy, c); err != nil { + _ = c.Close() + return nil, err + } + } + + return c, nil +} diff --git a/libs/capability/runner_test.go b/libs/capability/runner_test.go new file mode 100644 index 000000000..4a2841ada --- /dev/null +++ b/libs/capability/runner_test.go @@ -0,0 +1,280 @@ +package capability + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/loop" +) + +// runArgs is the arguments of one run over settings a test can start: a real port, and everything +// else off. +func runArgs(t *testing.T, ctor any) (*config, constructor, uint16) { + t.Helper() + + c, err := newConstructor(ctor) + require.NoError(t, err) + + port := uint16(freePort(t)) + cfg := defaultConfig() + cfg.observability.http.Port = port + // A stub the registrar's announcement can land on: registering the capability dials the + // proxy, so it has to be a real listener rather than merely a lazy one. + cfg.capabilities.ProxyURL = serveStubRegistry(t, &stubRegistry{adds: map[string]string{}}) + + return cfg, c, port +} + +// runToCompletion starts a run and stops it once it is serving, returning what run returned. +// +// A run blocks until its context is cancelled, so a test that wants one to finish has to be the +// thing that ends it - standing in for the signal an operator would send. A run that fails on the +// way up never gets as far as serving and reports that instead. +func runToCompletion(t *testing.T, cfg *config, c constructor, port uint16) error { + t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := make(chan error, 1) + go func() { done <- run(ctx, logger.Test(t), "cron", cfg, c) }() + + deadline := time.After(30 * time.Second) + for { + select { + case err := <-done: + return err + case <-deadline: + cancel() + t.Fatal("the run never started serving") + return nil + case <-time.After(5 * time.Millisecond): + if serving(int(port)) { + cancel() + return <-done + } + } + } +} + +// TestRunRunsAndUnwinds is run's whole contract in one: it brings the process up, and a run that has +// finished has put back everything it changed. +// +// The port is what makes the unwind visible. A web server that was serving and is not any more is +// one that gave the port back, which nothing else in the run reports. +func TestRunRunsAndUnwinds(t *testing.T) { + built := 0 + cfg, c, port := runArgs(t, func() *fake { built++; return newFake() }) + + require.NoError(t, runToCompletion(t, cfg, c, port)) + assert.Equal(t, 1, built) + + assertPortFree(t, port) +} + +// TestRunStartsTheCapability is what the root service buys: the capability is not merely built, it +// is running - which is what makes a health check mean anything. +func TestRunStartsTheCapability(t *testing.T) { + f := newFake() + cfg, c, port := runArgs(t, func() *fake { return f }) + + require.NoError(t, runToCompletion(t, cfg, c, port)) + + assert.True(t, f.started, "the run should have started the capability") + assert.True(t, f.closed, "and closed it again on the way out") +} + +// TestRunInstallsTelemetryBeforeBuildingTheCapability is the reason telemetry is started where it +// is, rather than with everything else the run supervises. +// +// A capability creates its instruments while it is being constructed - cron does, in NewMetrics - +// and an OTEL instrument resolves beholder.GetMeter() once, at creation. A capability built before +// this client became the process's would hold a noop meter for the life of the process, recording +// nothing, with no error anywhere to say so. +// +// Slow, and unavoidably so: it is the only test that builds a real beholder client, and closing one +// flushes to a collector that is not there - about 20s of export timeouts. The timeouts are on +// beholder.Config, which beholderConfig builds from the settings, so shortening them would mean a +// knob in production code that exists only for this. +func TestRunInstallsTelemetryBeforeBuildingTheCapability(t *testing.T) { + if testing.Short() { + t.Skip("builds a real beholder client; ~20s of export timeouts on close") + } + + cfg, _, port := runArgs(t, newFake) + // Nothing is listening there, which is fine: the client is built and installed without dialling. + cfg.observability.telemetry.Endpoint = fmt.Sprintf("localhost:%d", freePort(t)) + + var duringBuild *beholder.Client + c, err := newConstructor(func() *fake { + duringBuild = beholder.GetClient() + return newFake() + }) + require.NoError(t, err) + + before := beholder.GetClient() + require.NoError(t, runToCompletion(t, cfg, c, port)) + + require.NotNil(t, duringBuild, "the constructor should have run") + assert.NotSame(t, before, duringBuild, "the capability was built before telemetry was installed") + assert.Same(t, before, beholder.GetClient(), "and the previous client should be back afterwards") +} + +// TestRunUnwindsWhatStartedBeforeAFailure covers a constructor that fails after the observability +// services have been built: the run reports it and leaves nothing of itself behind. +// +// The beholder assertion below holds trivially here and is not evidence about telemetry. These +// settings configure no endpoint, so newTelemetry returns the noop service and installs nothing - +// there is nothing to put back. A run with telemetry configured would fail this if it asserted +// anything, which is the gap documented on the telemetry block in run: the global is swapped when +// telemetry is built, and the undo only works once the root has started it. +func TestRunUnwindsWhatStartedBeforeAFailure(t *testing.T) { + before := beholder.GetClient() + cfg, c, port := runArgs(t, func() (*fake, error) { return nil, errors.New("no schedule") }) + require.Empty(t, cfg.observability.telemetry.Endpoint, "these settings leave telemetry off") + + require.ErrorContains(t, runToCompletion(t, cfg, c, port), "no schedule") + + assert.Same(t, before, beholder.GetClient(), "nothing was installed, so nothing changed") + // The web server never got as far as starting, so the port was never taken. + assertPortFree(t, port) +} + +// TestRunReportsWhereItFailed pins that a failure names the service that would not start, rather +// than arriving as something further in. +// +// It is also what proves run gets as far as the web server on the configured port at all. +func TestRunReportsWhereItFailed(t *testing.T) { + cfg, c, port := runArgs(t, newFake) + + // Something else already has the port the web server is configured for. + taken, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + require.NoError(t, err) + t.Cleanup(func() { _ = taken.Close() }) + + require.ErrorContains(t, runToCompletion(t, cfg, c, port), + fmt.Sprintf("failed to listen on port %d", port)) +} + +// TestRunStaysUpUntilItIsToldToStop is the whole point of the wait: a run keeps serving until +// something ends it, and only then unwinds. +// +// It is also the only test that looks at a run from outside while it is up: everything is still +// serving when the assertions below are made. +func TestRunStaysUpUntilItIsToldToStop(t *testing.T) { + cfg, c, port := runArgs(t, newFake) + ctx, cancel := context.WithCancel(context.Background()) + + done := make(chan error, 1) + go func() { done <- run(ctx, logger.Test(t), "cron", cfg, c) }() + + require.Eventually(t, func() bool { return serving(int(port)) }, 30*time.Second, 5*time.Millisecond, + "the run should be serving while it is up") + + // The root is started, and the health checker reports on the root - so a run that is up is a + // run that says it is ready. This is what starting the capability bought. + for path, want := range map[string]int{"/healthz": 200, "/readyz": 200} { + res, err := http.Get(fmt.Sprintf("http://localhost:%d%s", port, path)) + require.NoError(t, err, path) + require.NoError(t, res.Body.Close()) + assert.Equal(t, want, res.StatusCode, path) + } + + // Still up: it has not unwound just because it finished starting. + select { + case err := <-done: + t.Fatalf("the run ended on its own: %v", err) + case <-time.After(50 * time.Millisecond): + } + + cancel() + select { + case err := <-done: + // Being asked to stop is not a failure: a binary that exited because it was told to should + // exit 0. + require.NoError(t, err) + case <-time.After(30 * time.Second): + t.Fatal("the run did not stop when it was told to") + } + + assertPortFree(t, port) +} + +// TestRunServesTheSettingsReloadEndpoint is the endpoint through the real run: the node dumps new +// settings to the file and hits /reload/settings.txt, and a 200 says every limit in the process now +// resolves against them. +// +// The handler reads the shared settings path - that path is the contract with the node - so this +// writes the real file and removes it again after: a leftover would be read by every other run in +// this package. +func TestRunServesTheSettingsReloadEndpoint(t *testing.T) { + require.NoError(t, os.MkdirAll(filepath.Dir(settingsPath()), 0o700)) + require.NoError(t, os.WriteFile(settingsPath(), []byte("[global]\n"), 0o600)) + t.Cleanup(func() { _ = os.Remove(settingsPath()) }) + + cfg, c, port := runArgs(t, newFake) + ctx, cancel := context.WithCancel(context.Background()) + + done := make(chan error, 1) + go func() { done <- run(ctx, logger.Test(t), "cron", cfg, c) }() + + require.Eventually(t, func() bool { return serving(int(port)) }, 30*time.Second, 5*time.Millisecond, + "the run should be serving while it is up") + + res, err := http.Post(fmt.Sprintf("http://localhost:%d%s", port, reloadPath()), "", nil) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + assert.Equal(t, http.StatusOK, res.StatusCode) + + cancel() + require.NoError(t, <-done) +} + +// TestUnderPluginHost covers the cookie go-plugin's Serve insists on, which is what decides whether +// a run hands the process to a host or waits for a signal. +// +// The check is necessary rather than defensive: Serve exits the process when the cookie is absent, +// so a standalone binary that called it anyway would die on startup. The other branch is not tested +// here - plugin.Serve takes over the process's stdio and handshake, so a test calling it would be +// testing go-plugin rather than this. +func TestUnderPluginHost(t *testing.T) { + h := loop.EmptyHandshakeConfig() + + t.Run("absent", func(t *testing.T) { + t.Setenv(h.MagicCookieKey, "") + assert.False(t, underPluginHost()) + }) + + t.Run("wrong value", func(t *testing.T) { + t.Setenv(h.MagicCookieKey, "not-the-cookie") + assert.False(t, underPluginHost()) + }) + + t.Run("present", func(t *testing.T) { + t.Setenv(h.MagicCookieKey, h.MagicCookieValue) + assert.True(t, underPluginHost()) + }) +} + +// assertPortFree reports whether nothing is listening on port, which is how a stopped web server is +// told from a running one. +func assertPortFree(t *testing.T, port uint16) { + t.Helper() + + l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if assert.NoError(t, err, "port %d should be free", port) { + require.NoError(t, l.Close()) + } +} diff --git a/libs/capability/server.go b/libs/capability/server.go new file mode 100644 index 000000000..96581d1a1 --- /dev/null +++ b/libs/capability/server.go @@ -0,0 +1,142 @@ +package capability + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "sync" + "sync/atomic" + + "google.golang.org/grpc" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// defaultHost is where a server binds and is advertised unless told otherwise. +const defaultHost = "localhost" + +// grpcConfig is where the gRPC servers this process opens bind and are advertised. +type grpcConfig struct { + AdvertiseHost string `usage:"host the gRPC servers this process opens bind to and 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. + 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"` +} + +type server struct { + services.Service + eng *services.Engine + + grpc *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{grpc: grpc.NewServer(), listener: listener} + s.Service, s.eng = services.Config{ + Name: "GRPCServer", + Start: s.start, + // No Close hook: stopping the server is what releases the Serve goroutine, and the engine + // waits for its goroutines before it would run the hook - so the stop lives in Close + // itself, ahead of the handoff. + }.NewServiceEngine(lggr) + + s.eng.Infow(fmt.Sprintf("Bound gRPC to port %s", s.port()), "address", s.address()) + return s, nil +} + +func (s *server) grpcServer() *grpc.Server { return s.grpc } + +// 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.grpc.Serve(s.listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) { + s.eng.Errorw("gRPC server stopped", "err", err) + } + }) + return nil +} + +// Close stops serving and releases the port, whether or not the server ever started. +// +// The stop comes before the handoff to the engine, not in its Close hook, because the engine +// closes in the other order: it waits for its goroutines before running the hook, and the Serve +// goroutine returns only once the server is stopped. Stopping from the hook would deadlock the +// two against each other. +// +// The two cases differ because the listener is opened by the constructor: a server that was +// started has the engine to unwind once serving has stopped; one that was not has no engine state +// at all, 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() { + s.grpc.GracefulStop() + return s.Service.Close() + } + s.grpc.Stop() + return s.listener.Close() +} + +// serverFactory makes gRPC servers, one per thing that has to be told apart by address - see the +// file 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: the ports are the process's, so a second counter would +// have two servers try to bind the same one. +type serverFactory 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 *serverFactory) 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 *serverFactory) nextPort() uint16 { + if f.startPort == 0 { + return 0 + } + f.mu.Lock() + defer f.mu.Unlock() + port := f.startPort + f.opened + f.opened++ + return port +} diff --git a/libs/capability/settings.go b/libs/capability/settings.go new file mode 100644 index 000000000..750d88b79 --- /dev/null +++ b/libs/capability/settings.go @@ -0,0 +1,117 @@ +package capability + +// CRE settings, and the limits resolved out of them. +// +// A capability binary runs under the empty LOOP: the node supervises its liveness over go-plugin but +// exposes no RPCs to it, so settings reach it through the filesystem instead. The node dumps each +// update to a conventional path and then hits this process's reload endpoint; both share a +// container, so os.TempDir() resolves to the same place on either side. +// +// The constants below are that convention, and are a copy of the ones in standalone/capability +// rather than a reference to them - importing that package would be a cycle, since it reaches back +// into the bootstrapper that imports this one. They have to keep the same values: the node writes +// the file, this reads it. + +import ( + "errors" + "fmt" + "io/fs" + "net/http" + "os" + "path/filepath" + + "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/settings/cresettings" + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" + "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 reload endpoint: /reload/. + // + // A limit's effective value is resolved out of this same payload, so there is no separate limits + // file: reloading this one reloads limits too. + settingsFileName = "settings.txt" + + // reloadPathPrefix is the route prefix reload requests are served 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, this process reads it. +func settingsPath() string { return filepath.Join(os.TempDir(), settingsDirName, settingsFileName) } + +// reloadPath is the route the node hits after dumping new settings: /reload/settings.txt. +func reloadPath() string { return reloadPathPrefix + settingsFileName } + +// 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") + } +} + +// newSettings builds this 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 +} + +// 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 +} + +// newLimitsFactory builds the limits factory over settings. +// +// AtomicSettings is a settings.Getter rather than a settings.Registry, so the factory polls it +// rather than subscribing - which is why swapping the getter is enough to make every limit in the +// process follow a reload. +// +// The meter is read here rather than captured earlier, so it has to be built after telemetry has +// installed itself: an instrument resolves its meter once, and one made against the noop client +// records nothing for the life of the process. +func newLimitsFactory(lggr logger.Logger, settings *loop.AtomicSettings) limits.Factory { + return limits.Factory{ + Settings: settings, + Meter: beholder.GetMeter(), + Logger: logger.Named(lggr, "Limits"), + } +} diff --git a/libs/capability/settings_test.go b/libs/capability/settings_test.go new file mode 100644 index 000000000..99292664e --- /dev/null +++ b/libs/capability/settings_test.go @@ -0,0 +1,102 @@ +package capability + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" +) + +// TestLoadSettingsWithoutAFile covers the state a binary starts in before the node has dumped +// anything: every limit resolves to its compiled-in default, which is the same state a LOOP is in +// before its first update arrives. +func TestLoadSettingsWithoutAFile(t *testing.T) { + s, err := newSettings(logger.Test(t)) + require.NoError(t, err) + require.NotNil(t, s) + + require.NoError(t, loadSettings(s, filepath.Join(t.TempDir(), "nothing-here.txt")), + "a missing file is not an error") +} + +// TestLoadSettingsReportsAnUnreadableFile covers the other side: a file that is there and cannot be +// used is a failure to start, not something to carry on past with defaults. +func TestLoadSettingsReportsAnUnreadableFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "settings.txt") + require.NoError(t, os.WriteFile(path, []byte("not a settings payload"), 0o600)) + + s, err := newSettings(logger.Test(t)) + require.NoError(t, err) + + require.ErrorContains(t, loadSettings(s, path), path) +} + +// TestSettingsPathIsTheSharedConvention pins the contract with the node: it writes this file and +// this process reads it, so the two have to resolve the same path. +// +// The constants are a copy of standalone/capability's - importing that package would be a cycle - +// so nothing but a test stops them drifting apart. +func TestSettingsPathIsTheSharedConvention(t *testing.T) { + assert.Equal(t, filepath.Join(os.TempDir(), "cre_limits", "settings.txt"), settingsPath()) + assert.Equal(t, "/reload/", reloadPathPrefix) +} + +// TestLimitsFactoryIsBuiltOverTheSettings covers the wiring the whole thing exists for: a limit made +// by this factory reads the settings this process holds, so a reload reaches it. +func TestLimitsFactoryIsBuiltOverTheSettings(t *testing.T) { + s, err := newSettings(logger.Test(t)) + require.NoError(t, err) + + f := newLimitsFactory(logger.Test(t), s) + + assert.Same(t, s, f.Settings, "the factory should poll the settings this process holds") + assert.NotNil(t, f.Meter) + assert.NotNil(t, f.Logger) +} + +// TestReloadHandlerSwapsTheSettings covers the endpoint the node hits after dumping new settings: a +// limit made from this process's factory resolves against the new payload on its next use, without +// anything having to be told. +func TestReloadHandlerSwapsTheSettings(t *testing.T) { + s, err := newSettings(logger.Test(t)) + require.NoError(t, err) + + // A global-scope setting, so no tenant is needed to resolve it. The compiled-in default is 0s. + f := newLimitsFactory(logger.Test(t), s) + limit, err := f.MakeTimeLimiter(cresettings.Default.TriggerRegistrationStatusUpdateTimeout) + require.NoError(t, err) + + path := filepath.Join(t.TempDir(), "settings.txt") + require.NoError(t, os.WriteFile(path, + []byte("[global]\nTriggerRegistrationStatusUpdateTimeout = \"15s\"\n"), 0o600)) + + w := httptest.NewRecorder() + reloadHandler(logger.Test(t), s, path)(w, httptest.NewRequest(http.MethodPost, reloadPath(), nil)) + require.Equal(t, http.StatusOK, w.Code) + + got, err := limit.Limit(t.Context()) + require.NoError(t, err) + assert.Equal(t, 15*time.Second, got, "the limit should resolve against the reloaded settings") +} + +// TestReloadHandlerKeepsThePreviousSettingsOnFailure covers the node's signal to retry later: a +// payload that cannot be applied is a 500, and what was in force stays in force. +func TestReloadHandlerKeepsThePreviousSettingsOnFailure(t *testing.T) { + s, err := newSettings(logger.Test(t)) + require.NoError(t, err) + + path := filepath.Join(t.TempDir(), "settings.txt") + require.NoError(t, os.WriteFile(path, []byte("not a settings payload"), 0o600)) + + w := httptest.NewRecorder() + reloadHandler(logger.Test(t), s, path)(w, httptest.NewRequest(http.MethodPost, reloadPath(), nil)) + assert.Equal(t, http.StatusInternalServerError, w.Code) +} diff --git a/libs/capability/telemetry.go b/libs/capability/telemetry.go new file mode 100644 index 000000000..f78d70548 --- /dev/null +++ b/libs/capability/telemetry.go @@ -0,0 +1,271 @@ +package capability + +import ( + "context" + "fmt" + "os" + "strings" + + prombridge "go.opentelemetry.io/contrib/bridges/prometheus" + "go.opentelemetry.io/otel/attribute" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +const ( + // 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 envPairs still picks those up. See TelemetryConfig.Attributes. + envTelemetryAttributePrefix = "CL_TELEMETRY_ATTRIBUTE_" + envTelemetryAuthHeaderPrefix = "CL_TELEMETRY_AUTH_HEADER_" +) + +// TelemetryConfig is the beholder client's configuration: 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 commonconfig.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"` +} + +// newTelemetry builds the beholder client and the service that owns it. +func newTelemetry(lggr logger.Logger, obs *observability) (*telemetryService, error) { + if obs.telemetry.Endpoint == "" { + return noopTelemetry(lggr), nil + } + + cfg, err := beholderConfig(lggr, obs) + if err != nil { + return nil, err + } + + client, err := beholder.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create beholder client: %w", err) + } + + return newTelemetryService(lggr, client, installGlobally(client)), nil +} + +func startTelemetry(ctx context.Context, lggr logger.Logger, obs *observability) (*telemetryService, error) { + telemetry, err := newTelemetry(lggr, obs) + if err != nil { + return nil, err + } + + return telemetry, telemetry.Start(ctx) +} + +// telemetryService is the beholder client and the process-global it is installed as, as one service +// - so that everything starting telemetry changes is undone by closing it. +// +// Installing happens when this is built rather than when it starts - see newTelemetry - so what +// closing undoes is something that was already done. The two are not symmetric, and cannot be: a +// capability binds its instruments while it is being constructed, which is before the root that +// starts this exists. +// +// The client is a sub-service rather than something this closes itself, which is what gets the +// order right. Sub-services start before the Start hook and close after the Close hook, so the +// client is running by the time it becomes the process's, and is still open when the globals are +// pointed back at the previous one - a late measurement reaches a live pipeline rather than a +// closed one. +type telemetryService struct { + services.Service + + // client is the beholder client this owns. The health checker mirrors itself through the same + // meter, so it has to be reachable from outside - which is the only reason it is a field. + client *beholder.Client +} + +// noopTelemetry is telemetry nobody configured: nothing to start, nothing to close, and no client +// behind it. The process keeps the noop beholder client it already had. +func noopTelemetry(lggr logger.Logger) *telemetryService { + t := &telemetryService{} + t.Service, _ = services.Config{Name: "Telemetry"}.NewServiceEngine(lggr) + return t +} + +func newTelemetryService(lggr logger.Logger, client *beholder.Client, restore func()) *telemetryService { + t := &telemetryService{client: client} + t.Service, _ = services.Config{ + Name: "Telemetry", + Close: func() error { restore(); return nil }, + NewSubServices: func(logger.Logger) []services.Service { + return []services.Service{client} + }, + }.NewServiceEngine(lggr) + return t +} + +// installGlobally makes client the process's beholder client, and returns what puts back the one +// that was there before. +// +// The global is the only lasting change starting telemetry makes - everything else it touches is +// owned by the client itself - so this is where reversing it has to be expressed. The otel +// providers are re-pointed on the way back as well as on the way in, since they are derived from +// whichever client is global and would otherwise keep pointing at the one being taken away. +func installGlobally(client *beholder.Client) func() { + previous := beholder.GetClient() + + beholder.SetClient(client) + beholder.SetGlobalOtelProviders() + + return func() { + beholder.SetClient(previous) + beholder.SetGlobalOtelProviders() + } +} + +// beholderConfig is the telemetry, tracing and chip ingress settings as the one client takes them. +func beholderConfig(lggr logger.Logger, obs *observability) (beholder.Config, error) { + cfg := beholder.DefaultConfig() + cfg.OtelExporterGRPCEndpoint = obs.telemetry.Endpoint + cfg.InsecureConnection = obs.telemetry.InsecureConnection + cfg.CACertFile = obs.telemetry.CACertFile + + attributes, err := envPairs(envTelemetryAttributePrefix, "telemetry.attributes", obs.telemetry.Attributes) + if err != nil { + return beholder.Config{}, err + } + for k, v := range attributes { + cfg.ResourceAttributes = append(cfg.ResourceAttributes, attribute.String(k, v)) + } + + cfg.AuthHeaders, err = envPairs(envTelemetryAuthHeaderPrefix, "telemetry.auth-headers", obs.telemetry.AuthHeaders) + if err != nil { + return beholder.Config{}, err + } + cfg.AuthPublicKeyHex = obs.telemetry.AuthPubKeyHex + cfg.AuthHeadersTTL = obs.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 + + if obs.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 = obs.chipIngress.Endpoint + cfg.ChipIngressEmitterEnabled = obs.chipIngress.Endpoint != "" + cfg.ChipIngressInsecureConnection = obs.chipIngress.InsecureConnection + + if obs.tracing.Enabled { + tracing := loop.TracingConfig{ + Enabled: true, + CollectorTarget: obs.telemetry.Endpoint, + SamplingRatio: obs.tracing.SamplingRatio, + TLSCertPath: obs.tracing.TLSCertFile, + // Not optional, despite reading like it. The exporter's dialer calls this on every + // failed dial without checking it is set, so leaving it nil turns an unreachable + // collector - a network blip, a collector restarting - into a nil dereference on a + // background goroutine, which takes the process with it. + OnDialError: func(err error) { + logger.Sugared(lggr).Errorw("Failed to dial the tracing collector", + "err", err, "target", obs.telemetry.Endpoint) + }, + } + if cfg.AuthHeaders != nil { + tracing.AuthHeaders = cfg.AuthHeaders + } + + exporter, err := tracing.NewSpanExporter() + if err != nil { + return beholder.Config{}, fmt.Errorf("failed to setup tracing exporter: %w", err) + } + cfg.TraceSpanExporter = exporter + cfg.TraceSampleRatio = tracing.SamplingRatio + } + + // Per the OTEL specification, histogram buckets must be defined when the client is created, so + // the views cannot be applied any later than this - which is why they are handed to the binary + // rather than asked of the capability, whose constructor has not run yet. See WithOtelViews. + cfg.MetricViews = obs.otelViews + + return cfg, nil +} + +// 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 nil, err + } + + merged := envMap(envPrefix) + if merged == nil { + return fromSetting, nil + } + for k, v := range fromSetting { + merged[k] = v + } + return merged, nil +} + +// envMap collects env vars starting with prefix into a map, with the prefix 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() { + 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 +} + +// 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/capability/telemetry_test.go b/libs/capability/telemetry_test.go new file mode 100644 index 000000000..164cab1b1 --- /dev/null +++ b/libs/capability/telemetry_test.go @@ -0,0 +1,401 @@ +package capability + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// settingsRoot builds the command tree the way RunErr does, through the same registration, and +// returns what the flags are bound to. +// +// The registration is the production one, so what these tests read is what a binary gets. +func settingsRoot(t *testing.T) (*cobra.Command, *config) { + t.Helper() + + root := &cobra.Command{Use: "cron"} + root.PersistentFlags().String("config", "", "Path to config file") + + // The production defaults, so what these read is what a binary gets rather than a zero value. + cfg := defaultConfig() + require.NoError(t, cfg.bind(root)) + return root, cfg +} + +// decoded runs args through the command tree and returns the settings as they were decoded. +func decoded(t *testing.T, args ...string) config { + t.Helper() + + root, cfg := settingsRoot(t) + root.AddCommand(&cobra.Command{Use: "run", RunE: func(*cobra.Command, []string) error { return nil }}) + + args = append([]string{"run"}, args...) + root.SetArgs(args) + require.NoError(t, root.Execute()) + return *cfg +} + +// TestTelemetryFlags pins the surface the beholder client's settings have: a flag per field, named +// after the service that owns it. A binary gets these without asking, because every binary has them. +func TestObservabilityFlags(t *testing.T) { + root, _ := settingsRoot(t) + + for _, name := range []string{ + "telemetry.endpoint", + "telemetry.insecure-connection", + "telemetry.ca-cert-file", + "telemetry.attributes", + "telemetry.auth-headers", + "telemetry.auth-pub-key-hex", + "telemetry.auth-headers-ttl", + "telemetry.prometheus-bridge-enabled", + + "tracing.enabled", + "tracing.sampling-ratio", + "tracing.tls-cert-file", + + "chip-ingress.endpoint", + "chip-ingress.insecure-connection", + + "pyroscope.server-address", + "pyroscope.auth-token", + "pyroscope.environment", + + "http.port", + + "capabilities.proxy-url", + "capabilities.capability-don-id", + } { + assert.NotNil(t, root.PersistentFlags().Lookup(name), "missing flag --%s", name) + } +} + +// TestCapabilitiesDecodeFromFlags covers the sibling section: what a capability binary needs from +// the node it runs beside, which is not observability and so is configured separately. +func TestCapabilitiesDecodeFromFlags(t *testing.T) { + cfg := decoded(t, "--capabilities.proxy-url", "dns:///registry.internal:9000", + "--capabilities.capability-don-id", "7") + + assert.Equal(t, "dns:///registry.internal:9000", cfg.capabilities.ProxyURL) + assert.Equal(t, uint32(7), cfg.capabilities.CapabilityDonID) +} + +// TestCapabilitiesAreOptional covers both settings being optional: a binary with no node behind it +// starts, and resolves only the capabilities it hosts. +// +// --http.port is the one thing a run still cannot do without, which is what the args below are. +func TestCapabilitiesAreOptional(t *testing.T) { + root, cfg := settingsRoot(t) + root.AddCommand(&cobra.Command{Use: "run", RunE: func(*cobra.Command, []string) error { return nil }}) + root.SetArgs([]string{"run", "--http.port", "1"}) + + require.NoError(t, root.Execute()) + assert.Empty(t, cfg.capabilities.ProxyURL) + assert.Zero(t, cfg.capabilities.CapabilityDonID) +} + +// TestObservabilityDefaults pins the settings that are not their zero value, since those are the +// ones an operator gets without asking. +func TestObservabilityDefaults(t *testing.T) { + root, _ := settingsRoot(t) + + sampling := root.PersistentFlags().Lookup("tracing.sampling-ratio") + require.NotNil(t, sampling) + assert.Equal(t, "1", sampling.DefValue, "sampling every trace is the default") + + port := root.PersistentFlags().Lookup("http.port") + require.NotNil(t, port) + assert.Equal(t, "8080", port.DefValue) +} + +// TestBinaryStartsWithNoSettingsAtAll is what defaulting the port bought: nothing has to be +// configured for a run to be a valid one. +// +// It is the whole surface in one assertion - every setting this binary has is now either defaulted +// or genuinely optional - so a flag gaining `validate:"required"` fails here. +func TestBinaryStartsWithNoSettingsAtAll(t *testing.T) { + root, cfg := settingsRoot(t) + root.AddCommand(&cobra.Command{Use: "run", RunE: func(*cobra.Command, []string) error { return nil }}) + root.SetArgs([]string{"run"}) + root.SilenceUsage, root.SilenceErrors = true, true + + require.NoError(t, root.Execute()) + assert.Equal(t, uint16(defaultHTTPPort), cfg.observability.http.Port) +} + +// TestTelemetryDecodesFromFlags is the point of registering them: what an operator types reaches the +// struct the beholder client is built from. +func TestObservabilityDecodesFromFlags(t *testing.T) { + cfg := decoded(t, + "--tracing.enabled", "true", + "--tracing.sampling-ratio", "0.25", + "--tracing.tls-cert-file", "/certs/trace.pem", + "--chip-ingress.endpoint", "chip:9000", + "--chip-ingress.insecure-connection", "true", + "--pyroscope.server-address", "pyro:4040", + "--pyroscope.environment", "staging", + "--telemetry.endpoint", "otel:4317", + "--telemetry.insecure-connection", "true", + "--telemetry.attributes", "env=staging", + "--telemetry.auth-headers-ttl", "5m", + "--telemetry.prometheus-bridge-enabled", "true", + ) + + assert.Equal(t, "otel:4317", cfg.observability.telemetry.Endpoint) + assert.True(t, cfg.observability.telemetry.InsecureConnection) + assert.Equal(t, []string{"env=staging"}, cfg.observability.telemetry.Attributes) + assert.Equal(t, 5*time.Minute, cfg.observability.telemetry.AuthHeadersTTL.Duration()) + assert.True(t, cfg.observability.telemetry.PrometheusBridgeEnabled) + + assert.True(t, cfg.observability.tracing.Enabled) + assert.InDelta(t, 0.25, cfg.observability.tracing.SamplingRatio, 0) + assert.Equal(t, "/certs/trace.pem", cfg.observability.tracing.TLSCertFile) + + assert.Equal(t, "chip:9000", cfg.observability.chipIngress.Endpoint) + assert.True(t, cfg.observability.chipIngress.InsecureConnection) + + assert.Equal(t, "pyro:4040", cfg.observability.pyroscope.ServerAddress) + assert.Equal(t, "staging", cfg.observability.pyroscope.Environment) +} + +// TestChipIngressFoldsIntoTheBeholderClient covers the shape of these three: chip ingress and +// tracing are configured separately but exported through the one client, so what turns them on is +// a field on its config rather than a service of their own. +func TestChipIngressFoldsIntoTheBeholderClient(t *testing.T) { + off, err := beholderConfig(logger.Test(t), &observability{telemetry: TelemetryConfig{Endpoint: "otel:4317"}}) + require.NoError(t, err) + assert.False(t, off.ChipIngressEmitterEnabled, "an unset endpoint leaves the emitter off") + + on, err := beholderConfig(logger.Test(t), &observability{ + telemetry: TelemetryConfig{Endpoint: "otel:4317"}, + chipIngress: ChipIngressConfig{Endpoint: "chip:9000", InsecureConnection: true}, + }) + require.NoError(t, err) + assert.True(t, on.ChipIngressEmitterEnabled, "setting an endpoint is what enables it") + assert.Equal(t, "chip:9000", on.ChipIngressEmitterGRPCEndpoint) + assert.True(t, on.ChipIngressInsecureConnection) +} + +// TestTracingFoldsIntoTheBeholderClient is the same for traces: they go to the telemetry endpoint, +// so enabling them adds an exporter to the client rather than standing anything else up. +func TestTracingFoldsIntoTheBeholderClient(t *testing.T) { + lggr, _ := detachedLogger() + + off, err := beholderConfig(lggr, &observability{telemetry: TelemetryConfig{Endpoint: "otel:4317"}}) + require.NoError(t, err) + assert.Nil(t, off.TraceSpanExporter) + + on, err := beholderConfig(lggr, &observability{ + telemetry: TelemetryConfig{Endpoint: "otel:4317"}, + tracing: TracingConfig{Enabled: true, SamplingRatio: 0.25}, + }) + require.NoError(t, err) + require.NotNil(t, on.TraceSpanExporter, "enabling tracing should add an exporter") + assert.InDelta(t, 0.25, on.TraceSampleRatio, 0) + + // The exporter dials its collector on a goroutine of its own, so it has to be shut down or it + // outlives the test that made it. + require.NoError(t, on.TraceSpanExporter.Shutdown(context.Background())) +} + +// TestTracingSurvivesAnUnreachableCollector covers the dialer's error path, which is not optional: +// loop.TracingConfig calls OnDialError without checking it is set, so an exporter built without one +// turns an unreachable collector into a nil dereference that takes the process with it. +func TestTracingSurvivesAnUnreachableCollector(t *testing.T) { + lggr, logs := detachedLogger() + + // A port nothing is listening on, so the dial is guaranteed to fail. + cfg, err := beholderConfig(lggr, &observability{ + telemetry: TelemetryConfig{Endpoint: fmt.Sprintf("localhost:%d", freePort(t))}, + tracing: TracingConfig{Enabled: true, SamplingRatio: 1}, + }) + require.NoError(t, err) + require.NotNil(t, cfg.TraceSpanExporter) + + // Exporting is what makes it dial, and the dial is asynchronous - so the failure is waited for + // rather than asserted straight away. Reaching this log at all is the point: without + // OnDialError the same path is a nil dereference, which would take the test binary with it + // rather than failing this assertion. + _ = cfg.TraceSpanExporter.ExportSpans(context.Background(), nil) + + require.Eventually(t, func() bool { + return len(logs.FilterMessage("Failed to dial the tracing collector").All()) > 0 + }, 10*time.Second, 10*time.Millisecond, "the dial failure should be reported rather than swallowed") + + require.NoError(t, cfg.TraceSpanExporter.Shutdown(context.Background())) +} + +// TestTelemetryDecodesFromEnv covers why the namespace and field names are what they are: a +// go-plugin host sets CL_TELEMETRY_ENDPOINT, and a binary it starts has to pick that up without +// being passed a flag. +func TestObservabilityDecodesFromEnv(t *testing.T) { + t.Setenv("CL_TELEMETRY_ENDPOINT", "from-env:4317") + + assert.Equal(t, "from-env:4317", decoded(t).observability.telemetry.Endpoint) +} + +// TestBeholderConfigMergesLegacyEnvPairs covers the other half of that contract: a host encodes a +// map as one env var per entry, and the setting wins where both name the same key. +func TestBeholderConfigMergesLegacyEnvPairs(t *testing.T) { + t.Setenv(envTelemetryAttributePrefix+"region", "us-east") + t.Setenv(envTelemetryAttributePrefix+"env", "from-env") + + cfg, err := beholderConfig(logger.Test(t), &observability{ + telemetry: TelemetryConfig{Endpoint: "otel:4317", Attributes: []string{"env=from-setting"}}, + }) + require.NoError(t, err) + + attributes := map[string]string{} + for _, a := range cfg.ResourceAttributes { + attributes[string(a.Key)] = a.Value.AsString() + } + assert.Equal(t, "us-east", attributes["region"], "the env-only entry should survive") + assert.Equal(t, "from-setting", attributes["env"], "the setting should win over the env var") +} + +// TestWithOtelViewsReachesTheClient is the whole of what the option is for: the views a binary +// passes are on the config the beholder client is built from. +// +// They cannot arrive any later - the OTEL specification requires histogram buckets at client +// creation - which is why this is an option on Run rather than something the capability declares. +func TestWithOtelViewsReachesTheClient(t *testing.T) { + view := sdkmetric.NewView( + sdkmetric.Instrument{Name: "cron_capability_*"}, + sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{Boundaries: []float64{1, 2, 3}}}, + ) + + cfg := defaultConfig() + cfg.observability.telemetry.Endpoint = "otel:4317" + WithOtelViews(view)(cfg) + + bcfg, err := beholderConfig(logger.Test(t), &cfg.observability) + require.NoError(t, err) + assert.Len(t, bcfg.MetricViews, 1, "the view should be on the config the client is built from") +} + +// TestWithOtelViewsDefaultsToNone covers a binary that passes none: the client keeps whatever +// beholder's own defaults are rather than being handed an empty list to mean something. +func TestWithOtelViewsDefaultsToNone(t *testing.T) { + obs := defaultObservability() + obs.telemetry.Endpoint = "otel:4317" + + cfg, err := beholderConfig(logger.Test(t), obs) + require.NoError(t, err) + assert.Empty(t, cfg.MetricViews) +} + +// TestRunUsesTheDecodedTelemetrySettings is the whole path in one: a flag on the command line +// reaches the config the beholder client is built from. +// +// A malformed attribute is what makes that visible without a reachable collector - building the +// config fails before a client is ever created - and an unregistered flag would have failed +// earlier, with cobra's "unknown flag" instead. +func TestRunUsesTheDecodedTelemetrySettings(t *testing.T) { + err := execute(t, logger.Test(t), newFake, "run", + "--telemetry.endpoint", "otel:4317", "--telemetry.attributes", "nope") + + require.ErrorContains(t, err, `invalid telemetry.attributes entry "nope": expected key=value`) +} + +// TestStartTelemetryWithoutAnEndpointChangesNothing is the off case: no endpoint means no client, +// so nothing is installed, nothing joins the root, and there is nothing to undo. +func TestStartTelemetryWithoutAnEndpointChangesNothing(t *testing.T) { + before := beholder.GetClient() + + telemetry, err := newTelemetry(logger.Test(t), &observability{}) + require.NoError(t, err) + + // A service that does nothing rather than a nil, so the caller has one thing to hand the root + // either way. No client behind it: the process keeps the noop beholder client it already had. + require.NotNil(t, telemetry) + assert.Nil(t, telemetry.client, "there is no client when telemetry is off") + assert.Same(t, before, beholder.GetClient(), "nothing should have been installed") + + require.NoError(t, telemetry.Start(t.Context())) + require.NoError(t, telemetry.Close()) + assert.Same(t, before, beholder.GetClient(), "and starting or closing it changes nothing") +} + +// TestTelemetryServiceRestoresTheGlobalWhenClosed is what making telemetry a service bought: what +// starting it changed about the process is undone by closing it, rather than by the run remembering +// to. +// TestTelemetryServiceRestoresTheGlobalWhenClosed is what making telemetry a service bought: the +// run hands it to the root and forgets about it, and closing it is what puts the process back. +func TestTelemetryServiceRestoresTheGlobalWhenClosed(t *testing.T) { + original := beholder.GetClient() + client := beholder.NewNoopClient() + require.NotSame(t, original, client) + + svc := newTelemetryService(logger.Test(t), client, installGlobally(client)) + require.Same(t, client, beholder.GetClient(), "installing happens when it is built, not when it starts") + + require.NoError(t, svc.Start(t.Context())) + require.NoError(t, svc.Close()) + assert.Same(t, original, beholder.GetClient(), "closing should put the previous client back") +} + +// TestInstallGloballyIsReversible is the reversibility itself: installing a client replaces the +// process's, and what comes back puts the previous one in its place. +// +// This is the only lasting change starting telemetry makes - everything else belongs to the client +// - so it is the thing worth pinning. It is tested here rather than through startBeholder because +// that would need a live OTLP endpoint to get as far as installing anything. +func TestInstallGloballyIsReversible(t *testing.T) { + original := beholder.GetClient() + client := beholder.NewNoopClient() + require.NotSame(t, original, client) + + restore := installGlobally(client) + assert.Same(t, client, beholder.GetClient(), "the new client should be the process's") + + restore() + assert.Same(t, original, beholder.GetClient(), "the previous client should be back") +} + +// TestInstallGloballyNests covers a second install on top of a first: each restore puts back what +// that install replaced, so unwinding in reverse arrives where it started. +func TestInstallGloballyNests(t *testing.T) { + original := beholder.GetClient() + first, second := beholder.NewNoopClient(), beholder.NewNoopClient() + + restoreFirst := installGlobally(first) + restoreSecond := installGlobally(second) + assert.Same(t, second, beholder.GetClient()) + + restoreSecond() + assert.Same(t, first, beholder.GetClient()) + + restoreFirst() + assert.Same(t, original, beholder.GetClient()) +} + +// TestRunLeavesTelemetryAsItFoundIt is the same guarantee seen from the binary: a run that started +// telemetry and finished has put the process back the way it was. +func TestRunLeavesTelemetryAsItFoundIt(t *testing.T) { + before := beholder.GetClient() + + require.NoError(t, execute(t, logger.Test(t), newFake, "run")) + + assert.Same(t, before, beholder.GetClient()) +} + +// detachedLogger is an observed logger that is not bound to t. +// +// A trace exporter dials its collector on a goroutine of its own and keeps retrying, so it outlives +// the test that made it - and logger.Test panics on anything logged after t has finished, which +// would make these tests fail for a reason that has nothing to do with what they check. +func detachedLogger() (logger.Logger, *observer.ObservedLogs) { + core, logs := observer.New(zapcore.ErrorLevel) + return logger.NewWithCores(core), logs +} diff --git a/libs/capability/webserver.go b/libs/capability/webserver.go new file mode 100644 index 000000000..32261e198 --- /dev/null +++ b/libs/capability/webserver.go @@ -0,0 +1,179 @@ +package capability + +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/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/services/otelhealth" + "github.com/smartcontractkit/chainlink-common/pkg/services/promhealth" +) + +// defaultHTTPPort is where the shared HTTP server listens unless it is told otherwise. +const defaultHTTPPort = 8080 + +// HTTPConfig is the shared HTTP server: /metrics, /debug/pprof, the health endpoints, and whatever +// routes a service registers on the mux while it is being built - it is not only prometheus's, so +// it is named for the transport it serves rather than for one of its handlers. +type HTTPConfig struct { + Port uint16 `usage:"port serving /metrics, /debug/pprof, /healthz, /readyz and any routes a service registers"` +} + +// newWebServer returns the shared HTTP server as a service. +func newWebServer(lggr logger.Logger, cfg HTTPConfig, mux *http.ServeMux, checker *services.HealthChecker) *webService { + 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)) + + w := &webService{ + lggr: lggr, + port: cfg.Port, + server: &http.Server{ + Handler: mux, + // Reasonable default based on a typical prometheus poll interval of 15s. + ReadTimeout: 5 * time.Second, + }, + } + w.Service, _ = services.Config{ + Name: "WebServer", + Start: w.start, + Close: w.close, + }.NewServiceEngine(lggr) + return w +} + +// startWebServer builds the shared HTTP server and starts it - listening is what Start does. +func startWebServer(ctx context.Context, lggr logger.Logger, cfg HTTPConfig, mux *http.ServeMux, checker *services.HealthChecker) (*webService, error) { + w := newWebServer(lggr, cfg, mux, checker) + return w, w.Start(ctx) +} + +type webService struct { + services.Service + + lggr logger.Logger + port uint16 + server *http.Server + + listener net.Listener +} + +func (w *webService) start(ctx context.Context) 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", w.port)) + if err != nil { + return fmt.Errorf("failed to listen on port %d: %w", w.port, err) + } + w.listener = listener + + go func() { + if err := w.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Sugared(w.lggr).Errorw("Metrics and health server stopped", "err", err) + } + }() + + w.lggr.Infow("Serving metrics and health endpoints", "address", listener.Addr().String()) + return nil +} + +func (w *webService) close() error { + // closes the listener too + return w.server.Close() +} + +// newHealthChecker returns the health checker as a service. +// +// client is always a client, never nil: when telemetry is off it is the noop one that is global +// until something replaces it, so the otel hooks are configured either way and simply record +// nothing. +func newHealthChecker(lggr logger.Logger, client *beholder.Client, reporters []services.HealthReporter) (*healthService, error) { + cfg := promhealth.ConfigureHooks(services.HealthCheckerConfig{}) + newCfg, err := otelhealth.ConfigureHooks(cfg, client.Meter) + if err != nil { + return nil, fmt.Errorf("failed to configure health checker otel hooks: %w", err) + } + cfg = newCfg + + h := &healthService{checker: cfg.New(), reporters: reporters} + h.Service, _ = services.Config{ + Name: "HealthChecker", + Start: h.start, + Close: h.close, + }.NewServiceEngine(lggr) + return h, nil +} + +func startHealthChecker(ctx context.Context, lggr logger.Logger, client *beholder.Client, reporters []services.HealthReporter) (*healthService, error) { + h, err := newHealthChecker(lggr, client, reporters) + if err != nil { + return nil, err + } + return h, h.Start(ctx) +} + +type healthService struct { + services.Service + + // checker is made when this is built, so that whatever serves its view can be given it without + // waiting for this to start. + checker *services.HealthChecker + reporters []services.HealthReporter +} + +func (h *healthService) start(context.Context) error { + if err := h.checker.Start(); err != nil { + return fmt.Errorf("failed to start health checker: %w", err) + } + // Registering reads a reporter's health as it goes, which is why this is here rather than at + // construction: everything being reported on has to be running by now. + for _, r := range h.reporters { + if err := h.checker.Register(r); err != nil { + // Started but not recorded as started, so nothing else will stop it. + return errors.Join( + fmt.Errorf("failed to register %s with the health checker: %w", r.Name(), err), + h.checker.Close()) + } + } + return nil +} + +func (h *healthService) close() error { return h.checker.Close() } + +// 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") + } + } +} diff --git a/libs/go.mod b/libs/go.mod index e47bc7838..4aff6277c 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -4,104 +4,160 @@ 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 + 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.20260807193849-47d010760510 + 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 + 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 - 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 + go.uber.org/zap v1.28.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 ) require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/XSAM/otelsql v0.37.0 // indirect + filippo.io/edwards25519 v1.2.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.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/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/buger/jsonparser v1.1.2 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // 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/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/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/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/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/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 + 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/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.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/grpc-ecosystem/grpc-gateway/v2 v2.28.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/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/jmoiron/sqlx v1.4.0 // indirect + github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // 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.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/mfridman/interpolate v0.0.2 // 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/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/pkg/errors v0.9.1 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // 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_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/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/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-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/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.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/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/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/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 + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect + github.com/urfave/cli/v2 v2.27.7 // 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 + go.mongodb.org/mongo-driver v1.17.9 // 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 @@ -110,13 +166,26 @@ 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 - golang.org/x/crypto v0.52.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // 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/mod v0.36.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-20260401024825-9d38bb4040a9 // indirect + golang.org/x/tools v0.45.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect gopkg.in/yaml.v3 v3.0.1 // 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 + +// 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 71323b09c..8d3d4d047 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -1,71 +1,161 @@ 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/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/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/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/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/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.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/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-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= +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/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/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/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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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.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= +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/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +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/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/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/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= 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-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +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-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-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-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/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -82,8 +172,12 @@ 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/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= @@ -94,29 +188,57 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN 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 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/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/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/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-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-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/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/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/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,8 +247,12 @@ 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/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.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= @@ -136,22 +262,23 @@ 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/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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/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= @@ -160,33 +287,67 @@ 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-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= +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/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +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 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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/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= +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/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= +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 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-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -196,41 +357,70 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +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/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/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/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.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -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/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/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/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/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.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/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/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= -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= @@ -240,44 +430,64 @@ 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/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/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/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +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= +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.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,15 +511,19 @@ 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= -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= @@ -330,19 +544,20 @@ 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= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -355,19 +570,17 @@ 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.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/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/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= @@ -384,8 +597,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= @@ -394,17 +605,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= @@ -421,12 +632,24 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/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/yaml.v2 v2.2.2/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= 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/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..f4bb9004f --- /dev/null +++ b/libs/ocr/oracle.go @@ -0,0 +1,180 @@ +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/ocrcommon" + "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] + + // 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 +} + +// 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() + } + + 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: 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/bootstrap_gen.go b/libs/standalone/bootstrap_gen.go new file mode 100644 index 000000000..3f666b424 --- /dev/null +++ b/libs/standalone/bootstrap_gen.go @@ -0,0 +1,761 @@ +// Code generated by github.com/smartcontractkit/capabilities/libs/standalone/gen, DO NOT EDIT. + +package standalone + +import ( + "context" + "fmt" + + common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// Run1 bootstraps the services built from 1 dependency. +// 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 what fn is handed already belongs to that +// instance. +func Run1[T0 any]( + bs *Bootstrapper, + fn func(ctx context.Context, cfg *StandaloneConfig, dep0 T0) []services.Service, + bootDep0 common.BootstrapDependency[T0], +) error { + return bs.run(func(index, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + 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 + } + }, + []common.BootstrapCommand{bootDep0}, + // The embedded form of each dependency, so its settings are registered on the command that + // 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)}, + ) +} + +// Run2 bootstraps the services built from 2 dependencies. +// 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 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 T0, dep1 T1) []services.Service, + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], +) error { + return bs.run(func(index, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + 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 + } + }, + []common.BootstrapCommand{bootDep0, bootDep1}, + // The embedded form of each dependency, so its settings are registered on the command that + // 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)}, + ) +} + +// Run3 bootstraps the services built from 3 dependencies. +// 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 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 T0, dep1 T1, dep2 T2) []services.Service, + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], +) error { + return bs.run(func(index, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + 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 + } + }, + []common.BootstrapCommand{bootDep0, bootDep1, bootDep2}, + // The embedded form of each dependency, so its settings are registered on the command that + // 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)}, + ) +} + +// Run4 bootstraps the services built from 4 dependencies. +// 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 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 T0, dep1 T1, dep2 T2, dep3 T3) []services.Service, + bootDep0 common.BootstrapDependency[T0], + bootDep1 common.BootstrapDependency[T1], + bootDep2 common.BootstrapDependency[T2], + bootDep3 common.BootstrapDependency[T3], +) error { + return bs.run(func(index, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + dep3 := instanceOf(bs, bootDep3, index, count, embed) + + 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 + } + }, + []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 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)}, + ) +} + +// Run5 bootstraps the services built from 5 dependencies. +// 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 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 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4) []services.Service, + 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, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + dep3 := instanceOf(bs, bootDep3, index, count, embed) + + dep4 := instanceOf(bs, bootDep4, index, count, embed) + + 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 + } + }, + []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 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)}, + ) +} + +// Run6 bootstraps the services built from 6 dependencies. +// 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 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 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5) []services.Service, + 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, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + dep3 := instanceOf(bs, bootDep3, index, count, embed) + + dep4 := instanceOf(bs, bootDep4, index, count, embed) + + dep5 := instanceOf(bs, bootDep5, index, count, embed) + + 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 + } + }, + []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 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)}, + ) +} + +// Run7 bootstraps the services built from 7 dependencies. +// 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 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 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6) []services.Service, + 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, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + dep3 := instanceOf(bs, bootDep3, index, count, embed) + + dep4 := instanceOf(bs, bootDep4, index, count, embed) + + dep5 := instanceOf(bs, bootDep5, index, count, embed) + + dep6 := instanceOf(bs, bootDep6, index, count, embed) + + 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 + } + }, + []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 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)}, + ) +} + +// Run8 bootstraps the services built from 8 dependencies. +// 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 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 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7) []services.Service, + 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, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + dep3 := instanceOf(bs, bootDep3, index, count, embed) + + dep4 := instanceOf(bs, bootDep4, index, count, embed) + + dep5 := instanceOf(bs, bootDep5, index, count, embed) + + dep6 := instanceOf(bs, bootDep6, index, count, embed) + + dep7 := instanceOf(bs, bootDep7, index, count, embed) + + 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 + } + }, + []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 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)}, + ) +} + +// Run9 bootstraps the services built from 9 dependencies. +// 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 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 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8) []services.Service, + 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, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + dep3 := instanceOf(bs, bootDep3, index, count, embed) + + dep4 := instanceOf(bs, bootDep4, index, count, embed) + + dep5 := instanceOf(bs, bootDep5, index, count, embed) + + dep6 := instanceOf(bs, bootDep6, index, count, embed) + + dep7 := instanceOf(bs, bootDep7, index, count, embed) + + dep8 := instanceOf(bs, bootDep8, index, count, embed) + + 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 + } + }, + []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 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)}, + ) +} + +// Run10 bootstraps the services built from 10 dependencies. +// 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 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 T0, dep1 T1, dep2 T2, dep3 T3, dep4 T4, dep5 T5, dep6 T6, dep7 T7, dep8 T8, dep9 T9) []services.Service, + 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, count int, embed bool) instanceServices { + + dep0 := instanceOf(bs, bootDep0, index, count, embed) + + dep1 := instanceOf(bs, bootDep1, index, count, embed) + + dep2 := instanceOf(bs, bootDep2, index, count, embed) + + dep3 := instanceOf(bs, bootDep3, index, count, embed) + + dep4 := instanceOf(bs, bootDep4, index, count, embed) + + dep5 := instanceOf(bs, bootDep5, index, count, embed) + + dep6 := instanceOf(bs, bootDep6, index, count, embed) + + dep7 := instanceOf(bs, bootDep7, index, count, embed) + + dep8 := instanceOf(bs, bootDep8, index, count, embed) + + dep9 := instanceOf(bs, bootDep9, index, count, embed) + + 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 + } + }, + []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 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 new file mode 100644 index 000000000..8dbc15d86 --- /dev/null +++ b/libs/standalone/bootstrapper.go @@ -0,0 +1,546 @@ +package standalone + +//go:generate go run ./gen + +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" + 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" + "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" + + "github.com/smartcontractkit/capabilities/libs/capability" +) + +// 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. Named after the + // instance when this process runs more than one. + Logger logger.SugaredLogger + + // 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 + + // 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 { + root *cobra.Command + settings settings + config *StandaloneConfig + commonConfig contract.CommonConfig + embedConfig embedConfig + observability observability + + profiler *pyroscope.Profiler // nil unless a pyroscope server is configured + + closersMu sync.Mutex + 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. 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 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 { + opt(&s) + } + + lggr, err := capability.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())) + + bs := &Bootstrapper{ + 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, 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 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 + 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 } + +// 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, 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. +// +// 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), 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() + + 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 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.`, + 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 + } + + if err := b.setupCommands(runCmd, embedCmd, configured, embedded); err != nil { + return err + } + + // 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) + + return b.root.Execute() +} + +// 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) + } + + // 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, count, embed)); err != nil { + stop() + return fmt.Errorf("failed to start instance %d: %w", i, err) + } + } + + 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) + + svcs, err := factory(ctx, cfg) + if err != nil { + return err + } + + root, _ := services.Config{ + Name: instanceName(index, count), + NewSubServices: func(logger.Logger) []services.Service { return svcs }, + }.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) + + // 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 { + return err + } + b.registerCloser(web) + + 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 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 { + cfg := *b.config + cfg.Mux = http.NewServeMux() + + 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 +} + +// 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) +} + +// 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 []contract.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 +} + +// 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 []contract.BootstrapCommand) ([]target, error) { + var targets []target + seen := map[any]bool{} + + var walk func(cmds []contract.BootstrapCommand) error + walk = func(cmds []contract.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 + } + } + 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 +// 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 +// that case and otherwise run standalone. +func underPluginHost() bool { + h := loop.EmptyHandshakeConfig() + return os.Getenv(h.MagicCookieKey) == h.MagicCookieValue +} + +// 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} } + +// 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 contract.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) { + v, err := d.bd.Get(ctx, d.bs.commonConfig) + if err == nil { + d.registerOnce.Do(func() { d.bs.registerCloser(v) }) + } + return v, err +} + +// 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, count int, embed bool) *dependency[T] { + if embed { + bd = bd.ForEmbedding(index, count) + } + return &dependency[T]{bs: bs, bd: bd} +} diff --git a/libs/standalone/capability/capability.go b/libs/standalone/capability/capability.go new file mode 100644 index 000000000..332b2443b --- /dev/null +++ b/libs/standalone/capability/capability.go @@ -0,0 +1,34 @@ +package capability + +import ( + "google.golang.org/protobuf/reflect/protoreflect" + + "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 + 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 new file mode 100644 index 000000000..5211054dc --- /dev/null +++ b/libs/standalone/capability/dependency.go @@ -0,0 +1,479 @@ +package capability + +import ( + "context" + "errors" + "fmt" + "net/http" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "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" + "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" + "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 + + // 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 + // 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 + // 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 + // 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 + // hub holds the trigger subscriptions the debug page has registered, shared + // the same way and for the same reason: a trigger registered across several + // instances is one subscription with a column per instance, not one each. + hub *ui.Hub +} + +// 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"` + + // 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 +// 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. +// +// 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, + servers: servers, + embeddedConfig: &embeddedConfig{CapabilityDonID: defaultEmbeddedDonID}, + fleet: &ui.Fleet{}, + hub: ui.NewHub(), + }) +} + +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 + + // 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 + // hub is shared for the same reason, one level further on: a subscription is + // registered on several instances and watched as one table, so the instances + // have to be delivering into one place. + hub *ui.Hub +} + +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 []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. +// +// 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, instances int) common.BootstrapDependency[Dependencies] { + return &embedded{ + lggr: d.lggr, + servers: d.servers.ForEmbedding(i, instances), + instances: instances, + cfg: d.embeddedConfig, + fleet: d.fleet, + hub: d.hub, + index: i, + } +} + +func (d *dependency) Get(ctx context.Context, cc 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) + } + + // 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 { + return Dependencies{}, fmt.Errorf("failed to get gRPC server factory: %w", err) + } + + return Dependencies{ + LimitsFactory: newLimitsFactory(d.lggr, settings), + CRESettings: settings, + CapabilityRegistry: proxy, + OCRConfigRegistry: proxy, + CapabilityDonID: d.CapabilityDonID, + lggr: d.lggr, + settings: settings, + settingsPath: SettingsPath(), + addresses: addresses, + servers: servers, + closers: []func() error{proxy.Close, conn.Close}, + httpDebug: d.HTTPDebug, + fleet: d.fleet, + hub: d.hub, + }, 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 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. +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)) + + 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) + } + svcs = append(svcs, newRegistrar(sc.Logger, dependencies, caps)) + 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) + } + + if err := ui.Mount(ui.Options{ + Mux: sc.Mux, + Prefix: ui.DefaultPrefix, + Server: server, + Fleet: dependencies.fleet, + Hub: dependencies.hub, + Index: dependencies.index, + Title: fmt.Sprintf("Capability debug (instance %d)", dependencies.index+1), + }); 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. +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 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 +// 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 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 { + 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 { + info, err := c.Info(ctx) + if err != nil { + return fmt.Errorf("failed to read capability %d info: %w", i, err) + } + // 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 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. 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 = append(r.hosted, hosted{id: info.ID, server: server}) + + 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.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 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 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 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 new file mode 100644 index 000000000..a54896df0 --- /dev/null +++ b/libs/standalone/capability/embed.go @@ -0,0 +1,139 @@ +package capability + +import ( + "context" + "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" + "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 + 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. + 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 + // hub is shared for the same reason: a trigger registered across several + // instances delivers into one subscription, so the table shows a column per + // instance rather than a table per instance. + hub *ui.Hub + // index is this instance's number, which names it on the fan-out page. + index int +} + +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 []common.BootstrapCommand{d.servers} +} + +// 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. +// +// 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, instances int) common.BootstrapDependency[Dependencies] { + return &embedded{ + lggr: d.lggr, + servers: d.servers.ForEmbedding(i, instances), + instances: instances, + cfg: d.cfg, + fleet: d.fleet, + hub: d.hub, + index: i, + } +} + +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 + } + + // 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", 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 + // still served - see registrar.serve. + return Dependencies{ + LimitsFactory: newLimitsFactory(d.lggr, settings), + CRESettings: settings, + 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, + // 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, + hub: d.hub, + index: d.index, + 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/capability/registry.go b/libs/standalone/capability/registry.go new file mode 100644 index 000000000..df6692d74 --- /dev/null +++ b/libs/standalone/capability/registry.go @@ -0,0 +1,197 @@ +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 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 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 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 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 { + 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 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 { + 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 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 + } + + 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 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 + } + return pick(r.proxy) +} + +// 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 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 + + 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/config.go b/libs/standalone/config.go new file mode 100644 index 000000000..b96087063 --- /dev/null +++ b/libs/standalone/config.go @@ -0,0 +1,148 @@ +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" + httpNamespace = "http" + + // 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"` +} + +// 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. +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"` +} + +// portFor is the port instance i serves on: the configured port plus i, so instances in one +// process do not collide over it. +func (c HTTPConfig) 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 { + telemetry TelemetryConfig + tracing TracingConfig + chipIngress ChipIngressConfig + pyroscope PyroscopeConfig + http HTTPConfig +} + +// defaultObservability is what the flags are bound to and decoded into, so an unset setting keeps +// 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}, + } +} + +// 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}, + {httpNamespace, &o.http}, + } +} + +// 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..d97f79112 --- /dev/null +++ b/libs/standalone/config_test.go @@ -0,0 +1,57 @@ +package standalone + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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)) +} + +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 new file mode 100644 index 000000000..b232e86c8 --- /dev/null +++ b/libs/standalone/db/db_dependency.go @@ -0,0 +1,182 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "io/fs" + "net/url" + "strings" + + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" +) + +// Config is the database: the connection settings it is opened with, and the schema this binary's +// tables live in. +// +// The connection settings are the database's own (sqlutil.Config, which also knows how to open it), +// inlined rather than nested so they keep the names every other binary gives them. +type Config struct { + //nolint:revive // struct-tag: "inline" is pkg/config/flags' squash option (Options.SquashTagOption), not a go-toml one + *sqlutil.Config `toml:",inline"` + + // Schema is where this binary's tables are created and read. Empty leaves the connection's own + // search path alone, which is right for a database of this binary's own. + // + // It is for the other case: a database shared with something else, usually the node's. A binary + // given a schema there cannot collide with the node's tables however plainly its own are named, + // and an operator can see at a glance which tables are whose. + Schema string `usage:"database schema this binary's tables are created in and read from; empty uses the connection's own search path"` +} + +// Dependency returns a standalone.BootstrapDependency that resolves an opened, migrated database. +// +// 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 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: &Config{Config: &sqlutil.Config{}}, + }) +} + +type dependency struct { + cfg *Config + migrationsFS fs.FS + migrationTable string +} + +func (d *dependency) Config() any { + return d.cfg +} + +func (d *dependency) Dependencies() []standalone.BootstrapCommand { + return []standalone.BootstrapCommand{} +} + +// 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)} +} + +// Get opens the database as configured, in the configured schema when there is one. +func (d *dependency) Get(ctx context.Context, _ standalone.CommonConfig) (*sql.DB, error) { + if d.cfg.Schema == "" { + return d.open(ctx, *d.cfg.Config, nil) + } + return d.openInSchema(ctx, d.cfg.Schema) +} + +// openInSchema opens the database with schema at the front of its search path, creating the schema if +// it is not already there. A configured schema and an embedded instance's schema both come through +// here: they want the same thing for different reasons. +// +// The schema is usually created by whoever owns the database - the node's migrations, for a binary +// the node launches - so the create is for the other case: a database of this binary's own, and the +// per-instance schemas of an embedded run, which no migration knows about. +func (d *dependency) openInSchema(ctx context.Context, schema string) (*sql.DB, error) { + cfg := *d.cfg.Config + var err error + if cfg.URL, err = withSearchPath(cfg.URL, schema); err != nil { + return nil, err + } + + return d.open(ctx, cfg, func(ctx context.Context, db *sql.DB) error { + if _, err := db.ExecContext(ctx, `CREATE SCHEMA IF NOT EXISTS `+quoteIdentifier(schema)); err != nil { + return fmt.Errorf("failed to create schema %s: %w", schema, err) + } + return 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 prepare != nil { + if err := prepare(ctx, db); err != nil { + return nil, err + } + } + + 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) { + // An instance's own schema, whatever the configured one was: instances of one run must not share + // tables, and a schema configured for all of them would be exactly that. + return d.openInSchema(ctx, d.schema) +} + +// 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..9f7d62749 --- /dev/null +++ b/libs/standalone/db/db_dependency_test.go @@ -0,0 +1,68 @@ +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: &Config{Config: &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, 2).(*embedded).schema) + assert.Equal(t, "node_1", template.ForEmbedding(1, 2).(*embedded).schema) + + // A configured schema does not survive embedding: it names one place for the binary's tables, + // and the instances of one run each need their own. + configured := &dependency{cfg: &Config{Config: &sqlutil.Config{URL: dsn}, Schema: "crecore"}, migrationTable: "migrations"} + assert.Equal(t, "node_1", configured.ForEmbedding(1, 2).(*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/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/eventstore/store.go b/libs/standalone/eventstore/store.go new file mode 100644 index 000000000..7783690bc --- /dev/null +++ b/libs/standalone/eventstore/store.go @@ -0,0 +1,172 @@ +// Package eventstore is where a trigger capability keeps the events it has sent +// but not had acknowledged. +// +// A trigger fires into a workflow that may not have run yet, may be restarting, +// or may never answer; the base trigger retransmits until it is acknowledged, and +// this is what it retransmits from. Holding that in memory would mean a restart +// dropping every event in flight, which is exactly when they matter - so it is a +// table, in the database the capability already has. +// +// The DDL is the capability's, not this package's: a binary owns its migrations, +// and this states what those migrations have to have made. See Schema. +package eventstore + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" +) + +// Table is where the events are kept, unqualified so that the connection's search +// path decides which schema that is - the same rule the rest of a capability's +// tables follow, and what keeps the instances of an embedded run from answering +// each other's events. +const Table = "trigger_pending_events" + +// Schema is the DDL this package expects, for a capability's migrations to +// include verbatim. +// +// It is a constant rather than a migration of its own because migrations belong +// to a binary: one goose history per database, owned by whoever runs it. What +// this package can do is say what shape it reads. +const Schema = `CREATE TABLE IF NOT EXISTS ` + Table + ` ( + scope TEXT NOT NULL DEFAULT '', + trigger_id TEXT NOT NULL, + event_id TEXT NOT NULL, + payload BYTEA NOT NULL, + first_at TIMESTAMPTZ NOT NULL, + last_sent_at TIMESTAMPTZ NULL, + attempts INTEGER NOT NULL DEFAULT 0, + org_id TEXT NOT NULL DEFAULT '', + PRIMARY KEY (scope, trigger_id, event_id) +)` + +// New returns the event store over ds, holding the events of one scope. +// +// A scope is whoever these events are owed to when the table is shared: two +// processes of the same capability on one schema - the EVM capability on two +// chains, say - would otherwise list, retransmit and delete each other's events, +// since a trigger ID says which workflow asked but not which chain it asked of. A +// capability with a table to itself passes "" and never thinks about it again. +func New(ds sqlutil.DataSource, scope string) capabilities.EventStore { + return &store{ds: ds, scope: scope} +} + +type store struct { + ds sqlutil.DataSource + scope string +} + +var _ capabilities.EventStore = (*store)(nil) + +func (s *store) Insert(ctx context.Context, rec capabilities.PendingEvent) error { + const q = `INSERT INTO ` + Table + ` (scope, trigger_id, event_id, payload, first_at, last_sent_at, attempts, org_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` + + var lastSent sql.NullTime + if !rec.LastSentAt.IsZero() { + lastSent = sql.NullTime{Time: rec.LastSentAt, Valid: true} + } + + if _, err := s.ds.ExecContext(ctx, q, + s.scope, rec.TriggerId, rec.EventId, rec.Payload, rec.FirstAt, lastSent, rec.Attempts, rec.OrgID, + ); err != nil { + return fmt.Errorf("failed to insert pending event trigger_id=%s event_id=%s: %w", rec.TriggerId, rec.EventId, err) + } + return nil +} + +// UpdateDelivery records another attempt at an event. +// +// An event that is no longer there answers with sql.ErrNoRows rather than +// silently doing nothing: it was acknowledged while this send was in flight, and +// the caller is entitled to know the difference between that and a write that +// landed. +func (s *store) UpdateDelivery(ctx context.Context, triggerID, eventID string, lastSentAt time.Time, attempts int) error { + const q = `UPDATE ` + Table + ` +SET last_sent_at = $4, attempts = $5 +WHERE scope = $1 AND trigger_id = $2 AND event_id = $3` + + var lastSent any + if !lastSentAt.IsZero() { + lastSent = lastSentAt + } + + res, err := s.ds.ExecContext(ctx, q, s.scope, triggerID, eventID, lastSent, attempts) + if err != nil { + return fmt.Errorf("failed to update delivery for trigger_id=%s event_id=%s: %w", triggerID, eventID, err) + } + rows, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("failed to read rows affected updating delivery for trigger_id=%s event_id=%s: %w", triggerID, eventID, err) + } + if rows == 0 { + return sql.ErrNoRows + } + return nil +} + +// List returns everything still unacknowledged, oldest first, which is the order +// a restart replays them in. +func (s *store) List(ctx context.Context) ([]capabilities.PendingEvent, error) { + const q = `SELECT trigger_id, event_id, payload, first_at, last_sent_at, attempts, org_id +FROM ` + Table + ` +WHERE scope = $1 +ORDER BY first_at ASC` + + type row struct { + TriggerID string `db:"trigger_id"` + EventID string `db:"event_id"` + Payload []byte `db:"payload"` + FirstAt time.Time `db:"first_at"` + LastSentAt sql.NullTime `db:"last_sent_at"` + Attempts int `db:"attempts"` + OrgID string `db:"org_id"` + } + + var rows []row + if err := s.ds.SelectContext(ctx, &rows, q, s.scope); err != nil { + return nil, fmt.Errorf("failed to list pending events: %w", err) + } + + events := make([]capabilities.PendingEvent, 0, len(rows)) + for _, r := range rows { + var lastSent time.Time + if r.LastSentAt.Valid { + lastSent = r.LastSentAt.Time + } + events = append(events, capabilities.PendingEvent{ + TriggerId: r.TriggerID, + EventId: r.EventID, + Payload: append([]byte(nil), r.Payload...), + FirstAt: r.FirstAt, + LastSentAt: lastSent, + Attempts: r.Attempts, + OrgID: r.OrgID, + }) + } + return events, nil +} + +// DeleteEvent forgets one event, which is what an acknowledgement means. +func (s *store) DeleteEvent(ctx context.Context, triggerID, eventID string) error { + const q = `DELETE FROM ` + Table + ` WHERE scope = $1 AND trigger_id = $2 AND event_id = $3` + if _, err := s.ds.ExecContext(ctx, q, s.scope, triggerID, eventID); err != nil { + return fmt.Errorf("failed to delete pending event trigger_id=%s event_id=%s: %w", triggerID, eventID, err) + } + return nil +} + +// DeleteEventsForTrigger forgets everything owed to a trigger, which is what +// unregistering it means: nothing is waiting for those events any more. +func (s *store) DeleteEventsForTrigger(ctx context.Context, triggerID string) error { + const q = `DELETE FROM ` + Table + ` WHERE scope = $1 AND trigger_id = $2` + if _, err := s.ds.ExecContext(ctx, q, s.scope, triggerID); err != nil { + return fmt.Errorf("failed to delete pending events for trigger_id=%s: %w", triggerID, err) + } + return nil +} diff --git a/libs/standalone/gen/bootstrap.go.tmpl b/libs/standalone/gen/bootstrap.go.tmpl new file mode 100644 index 000000000..53f7d04c5 --- /dev/null +++ b/libs/standalone/gen/bootstrap.go.tmpl @@ -0,0 +1,54 @@ +package standalone + +import ( + "context" + "fmt" + + common "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + "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 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 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{{.}} T{{.}}, {{end}}) []services.Service, + {{- range RangeNum .}} + bootDep{{.}} common.BootstrapDependency[T{{.}}], + {{- end}} +) error { + return bs.run(func(index, count int, embed bool) instanceServices { + {{range RangeNum .}} + dep{{.}} := instanceOf(bs, bootDep{{.}}, index, count, embed) + {{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 + } + }, + []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 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/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/grpc/dependency.go b/libs/standalone/grpc/dependency.go new file mode 100644 index 000000000..9ea5ab5ec --- /dev/null +++ b/libs/standalone/grpc/dependency.go @@ -0,0 +1,175 @@ +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 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 +} + +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, 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) { + // 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 +// 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..8ba2315cb --- /dev/null +++ b/libs/standalone/grpc/dependency_test.go @@ -0,0 +1,138 @@ +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, 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{}) + 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, 2).Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + 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") + + 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/keystore/embed.go b/libs/standalone/keystore/embed.go new file mode 100644 index 000000000..219c04931 --- /dev/null +++ b/libs/standalone/keystore/embed.go @@ -0,0 +1,66 @@ +package keystore + +import ( + "context" + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// Embedded builds the keystore instance i of an embedded run signs with. +// +// It is a parameter rather than something this package does, because a key is a +// chain's: deriving an EVM account means secp256k1 and an address, deriving a +// Solana one means something else, and neither belongs in the package that only +// knows how to ask another process for a signature. A capability passes the one +// its chain provides - see chainlink-evm's cre/evmchain for the EVM one. +// +// A capability with no answer to this passes nil, and an embedded run of it fails +// where it is resolved rather than at the first signature. +type Embedded func(instance int) (core.Keystore, error) + +// embedded is one embedded instance's keystore: keys of its own, since embedding +// replaces the node it would otherwise borrow them from. +type embedded struct { + lggr commonlogger.Logger + build Embedded + index int +} + +var _ standalone.BootstrapDependency[core.Keystore] = (*embedded)(nil) + +func (d *embedded) Namespace() string { return namespace } + +// Config is nothing at all: there is no address to dial, and the keys come from +// the instance index. +func (d *embedded) Config() any { return nil } + +func (d *embedded) Dependencies() []standalone.BootstrapCommand { return nil } + +// ForEmbedding returns the dependency of instance i, so an already-embedded +// dependency embedded again is instance i's rather than a copy of this one's. +func (d *embedded) ForEmbedding(i, _ int) standalone.BootstrapDependency[core.Keystore] { + return &embedded{lggr: d.lggr, build: d.build, index: i} +} + +func (d *embedded) Get(context.Context, standalone.CommonConfig) (core.Keystore, error) { + if d.build == nil { + return nil, errors.New("this binary has no keys of its own to run embedded with: it signs with the node's, and an embedded run has no node") + } + + keystore, err := d.build(d.index) + if err != nil { + return nil, fmt.Errorf("failed to build the keys of instance %d: %w", d.index, err) + } + + accounts, err := keystore.Accounts(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to read the accounts of instance %d: %w", d.index, err) + } + d.lggr.Infow("Signing with keys of this instance's own", "instance", d.index, "accounts", accounts) + + return keystore, nil +} diff --git a/libs/standalone/keystore/proxy.go b/libs/standalone/keystore/proxy.go new file mode 100644 index 000000000..07ac868a2 --- /dev/null +++ b/libs/standalone/keystore/proxy.go @@ -0,0 +1,139 @@ +// Package keystore is how a capability signs with keys it does not hold. +// +// A capability binary runs beside the node rather than inside it, and the keys +// that say who the node is stay with the node - or, in this framework, with the +// process fronting it. What a capability gets instead is this: the signing, over +// gRPC, of digests it computed itself, by an account the on-chain registry +// already knows this node by. +// +// It resolves to chainlink-common's core.Keystore, which is what everything +// signing for a chain is written against - chainlink-evm's transaction manager +// included - so what a capability does with it is hand it over unchanged. +package keystore + +import ( + "context" + "errors" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + creproxy "github.com/smartcontractkit/chainlink-protos/cre/impl/proxy" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// namespace groups this dependency's settings under keystore.*. +// +// Its own rather than shared with ocr.*: the two happen to be served by one +// process today, but what they ask for differs - one is the identity a protocol +// runs under, the other is the account a chain is written to - and a deployment +// that ever splits them would need to say so with two addresses. +const namespace = "keystore" + +// Config is where the keys are. +// +// Not `validate:"required"`: an embedded instance signs with keys derived from +// its index and has no address to dial, so the rule is checked when this form is +// resolved rather than tagged on a field both forms share. +type Config struct { + ProxyAddress string `usage:"gRPC address of the process holding this node's chain keys, which signs on this capability's behalf; required outside embed"` +} + +// Proxy returns the standalone.BootstrapDependency a capability resolves to sign +// as the node it runs beside. +// +// Resolving it dials the holder and asks which accounts it has. That call is the +// point: a capability configured to transmit from an account the node does not +// hold is misconfigured, and finding that out while starting up is better than +// finding it out from the first transaction that needed a signature. +// +// embedded is what an embedded run signs with instead, since it has no node to +// borrow from; nil says this binary cannot run embedded. See Embedded. +func Proxy(lggr commonlogger.Logger, embedded Embedded) standalone.BootstrapDependency[core.Keystore] { + // Wrapped so the connection is dialled at most once however many services resolve this. + return standalone.OnceBootstrapper[core.Keystore](&proxyDependency{lggr: lggr, embedded: embedded}) +} + +type proxyDependency struct { + lggr commonlogger.Logger + embedded Embedded + cfg Config +} + +var _ standalone.BootstrapDependency[core.Keystore] = (*proxyDependency)(nil) + +func (d *proxyDependency) Namespace() string { return namespace } + +func (d *proxyDependency) Config() any { return &d.cfg } + +func (d *proxyDependency) Dependencies() []standalone.BootstrapCommand { return nil } + +// ForEmbedding returns the in-process form: an embedded run has no node to borrow +// an account from, so instance i signs with keys of its own. See embedded. +func (d *proxyDependency) ForEmbedding(i, _ int) standalone.BootstrapDependency[core.Keystore] { + return &embedded{lggr: d.lggr, build: d.embedded, index: i} +} + +func (d *proxyDependency) Get(ctx context.Context, _ standalone.CommonConfig) (core.Keystore, error) { + if d.cfg.ProxyAddress == "" { + return nil, errors.New("--keystore.proxy-address is required to sign with the node's keys") + } + + // grpc.NewClient does not connect: the Accounts call below is what finds out + // whether anything is there, which is why it is made here rather than left to + // the first signature. + conn, err := grpc.NewClient(d.cfg.ProxyAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("failed to create keystore client for %s: %w", d.cfg.ProxyAddress, err) + } + + remote := &remoteKeystore{client: creproxy.NewKeystoreClient(conn), closer: conn.Close} + + accounts, err := remote.Accounts(ctx) + if err != nil { + _ = conn.Close() + return nil, fmt.Errorf("failed to read the accounts held at %s: %w", d.cfg.ProxyAddress, err) + } + + d.lggr.Infow("Signing with the node's keys", "keystoreAddress", d.cfg.ProxyAddress, "accounts", accounts) + + return remote, nil +} + +// remoteKeystore is core.Keystore over the connection to whoever holds the keys. +type remoteKeystore struct { + client creproxy.KeystoreClient + closer func() error +} + +var _ core.Keystore = (*remoteKeystore)(nil) + +func (k *remoteKeystore) Accounts(ctx context.Context) ([]string, error) { + reply, err := k.client.Accounts(ctx, &creproxy.AccountsRequest{}) + if err != nil { + return nil, err + } + return reply.GetAccounts(), nil +} + +func (k *remoteKeystore) Sign(ctx context.Context, account string, data []byte) ([]byte, error) { + reply, err := k.client.Sign(ctx, &creproxy.SignRequest{Account: account, Data: data}) + if err != nil { + return nil, err + } + return reply.GetSigned(), nil +} + +// Decrypt is not served: the keys lent out here sign, and a capability asking to +// decrypt with one is asking for something the holder will not do either. +func (k *remoteKeystore) Decrypt(context.Context, string, []byte) ([]byte, error) { + return nil, errors.New("the node's chain keys sign; they do not decrypt") +} + +// Close releases the connection. The bootstrapper closes resolved dependency +// values on shutdown, after the services built from them. +func (k *remoteKeystore) Close() error { return k.closer() } diff --git a/libs/standalone/ocr/dependency.go b/libs/standalone/ocr/dependency.go new file mode 100644 index 000000000..8b3bfa56a --- /dev/null +++ b/libs/standalone/ocr/dependency.go @@ -0,0 +1,128 @@ +// 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). +// +// There are two, mirroring the two halves of core's SingletonPeerWrapper, and a binary picks one +// by calling its constructor: +// +// - 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. +// +// 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 ( + "io" + + "github.com/smartcontractkit/libocr/commontypes" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" +) + +// 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. + OCR3_1Endpoint ocr2types.BinaryNetworkEndpoint2Factory + + // 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 + + 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 *Factories) Close() error { + if f == nil || f.closer == nil { + return nil + } + return f.closer.Close() +} + +// 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 + + // 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 + + // 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 + + // 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. +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/embed.go b/libs/standalone/ocr/embed.go new file mode 100644 index 000000000..b58b64a63 --- /dev/null +++ b/libs/standalone/ocr/embed.go @@ -0,0 +1,118 @@ +package ocr + +import ( + "context" + "fmt" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + commonlogger "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/standalone" +) + +// 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 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. +// +// 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[*OCRFactories] = (*embedded)(nil) + +// 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 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 + // 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, instances int) standalone.BootstrapDependency[*OCRFactories] { + return &embedded{lggr: d.lggr, index: i, instances: instances} +} + +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 + } + + // 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. + keyrings, err := EmbeddedKeyrings(d.index) + if err != nil { + return nil, err + } + bundle, err := EmbeddedOCR2Bundle(d.index) + if err != nil { + return nil, err + } + + 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, + // 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..a9b4bfc8b --- /dev/null +++ b/libs/standalone/ocr/embed_config.go @@ -0,0 +1,329 @@ +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 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: 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, + // 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, + + // 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 +// 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..d00e71939 --- /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/inproc.go b/libs/standalone/ocr/inproc.go new file mode 100644 index 000000000..7de586db6 --- /dev/null +++ b/libs/standalone/ocr/inproc.go @@ -0,0 +1,352 @@ +package ocr + +import ( + "fmt" + "math" + "slices" + "sync" + + "github.com/smartcontractkit/libocr/commontypes" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +// 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). 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 +// mailboxes keyed by config digest and peer ID. +type network struct { + 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 +// 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{}, + } +} + +// 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 +} + +// 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]) +} + +// 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 { + 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]) +} + +// 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 { + 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) +} diff --git a/libs/standalone/ocr/inproc_test.go b/libs/standalone/ocr/inproc_test.go new file mode 100644 index 000000000..d8778db30 --- /dev/null +++ b/libs/standalone/ocr/inproc_test.go @@ -0,0 +1,192 @@ +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" +) + +// 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()) + }) + + // 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{0, 1, 2} { + msg := receive(t, endpoints[i].Receive()) + assert.Equal(t, []byte("to everyone"), msg.Msg, "oracle %d", i) + assert.Equal(t, commontypes.OracleID(0), msg.Sender, "oracle %d", i) + } + }) + + 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)) + }) +} + +// 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 new file mode 100644 index 000000000..f365126f6 --- /dev/null +++ b/libs/standalone/ocr/keyring.go @@ -0,0 +1,148 @@ +package ocr + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "fmt" + "strconv" + "sync" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" +) + +// 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))) + 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 the peer public key: %w", err) + } + return &peerKeyring{signer: signer, 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 +} + +// 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) } + +// EmbeddedKeyrings is instance i's OCR identity as the two keyrings an oracle signs with, which is +// the shape a hosted peer hands back for the node's keys too - so a caller does not have to know +// which kind of run it is in. +func EmbeddedKeyrings(i int) (Keyrings, error) { + bundle, err := EmbeddedOCR2Bundle(i) + if err != nil { + return Keyrings{}, err + } + onchain, err := ocr2key.NewOCR3Keyring(EVMFamily, bundle) + if err != nil { + return Keyrings{}, fmt.Errorf("failed to build the onchain keyring of instance %d: %w", i, err) + } + // The bundle is already an offchain keyring, so only the onchain half is adapted. + return Keyrings{Offchain: bundle, Onchain: onchain}, 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 +} + +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/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..b6a620c9c --- /dev/null +++ b/libs/standalone/ocr/proxy.go @@ -0,0 +1,155 @@ +package ocr + +import ( + "context" + "errors" + "fmt" + + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "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" + + "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 +// 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'"` + + // 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. + // + // 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 +// 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[*OCRFactories] { + // Wrap in OnceBootstrapper so Get (which dials the proxy) runs at most once even if several + // services resolve this dependency. + return standalone.OnceBootstrapper[*OCRFactories](&proxyDependency{lggr: lggr}) +} + +type proxyDependency struct { + lggr commonlogger.Logger + + cfg ProxyConfig +} + +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. +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, 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) { + 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) + } + + // 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{ + 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}, + Bootstrappers: d.cfg.Bootstrappers.ToBootstrapperLocators(), + }, nil +} diff --git a/libs/standalone/ocr/proxy_test.go b/libs/standalone/ocr/proxy_test.go new file mode 100644 index 000000000..3fd873d4c --- /dev/null +++ b/libs/standalone/ocr/proxy_test.go @@ -0,0 +1,176 @@ +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" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/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) + + // 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: serveSigner(t), + PeerID: peerID, + TransmitAccount: "0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437", + }} + + 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()) + + // 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("the onchain key is announced exactly as the signer serves it", func(t *testing.T) { + // The signer holds the keyring, and a keyring's public key is already the + // multichain form a configuration lists (ocr2key.NewOCR3Keyring). Encoding it + // again here produced 01 17 00 01 14 00
against a config carrying + // 01 14 00
: an oracle whose offchain key matches and whose onchain key + // does not. + dep := &proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ + ProxyAddress: serveSigner(t), + PeerID: mustPeerID(t, 7), + TransmitAccount: "0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437", + }} + + factories, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, factories.Close()) }) + + assert.Equal(t, ocr2types.OnchainPublicKey(stubOnchainPublicKey), factories.Onchain.PublicKey()) + }) + + t.Run("a signer serving an unencoded key is rejected", func(t *testing.T) { + dep := &proxyDependency{lggr: logger.Test(t), cfg: ProxyConfig{ + ProxyAddress: serve(t, bareSigner{}), + PeerID: mustPeerID(t, 7), + TransmitAccount: "0x5994a5155e9b81ab7794b79bfbf076ef5ef7c437", + }} + + _, err := dep.Get(t.Context(), standalone.CommonConfig{}) + require.ErrorContains(t, err, "not the encoded form a configuration lists") + }) + + 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 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") + + // 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 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") + }) +} + +// 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() + return serve(t, stubSigner{}) +} + +func serve(t *testing.T, signer proxy.SignerServer) string { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + server := grpc.NewServer() + proxy.RegisterSignerServer(server, signer) + 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), + // The encoded form, which is what an onchain keyring's public key is and + // therefore what the signer holding one serves. + OnchainPublicKey: stubOnchainPublicKey, + MaxSignatureLength: 65, + }, nil +} + +// stubOnchainPublicKey is an EVM address in the form a configuration lists it. +var stubOnchainPublicKey = append([]byte{0x01, 0x14, 0x00}, make([]byte, 20)...) + +// bareSigner serves the address itself rather than the encoded form: the mistake +// this guards against, since an oracle announcing a key wrapped twice matches no +// configuration and says so only once a DON has refused it. +type bareSigner struct { + proxy.UnimplementedSignerServer +} + +func (bareSigner) Keys(context.Context, *proxy.KeysRequest) (*proxy.KeysReply, error) { + return &proxy.KeysReply{ + OffchainPublicKey: make([]byte, ed25519.PublicKeySize), + ConfigEncryptionPublicKey: make([]byte, ed25519.PublicKeySize), + OnchainPublicKey: make([]byte, 20), + MaxSignatureLength: 65, + }, nil +} diff --git a/libs/standalone/ocr/signer.go b/libs/standalone/ocr/signer.go new file mode 100644 index 000000000..d9186f9be --- /dev/null +++ b/libs/standalone/ocr/signer.go @@ -0,0 +1,202 @@ +package ocr + +import ( + "context" + "errors" + "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) + } + + // Taken as it is served, not re-encoded: an onchain keyring's public key is + // already the multichain form a configuration lists it as (see + // ocr2key.NewOCR3Keyring), and the signer holds the keyring. Encoding it again + // here would announce a key wrapped twice, which no configuration carries, and + // which is what an oracle whose offchain key matches but whose onchain key does + // not looks like. + // + // Checked rather than trusted, because the difference is invisible until a DON + // refuses to recognise this oracle: what comes back has to decode as the form it + // claims to be. + onchainPublicKey := ocrtypes.OnchainPublicKey(keys.GetOnchainPublicKey()) + if _, err := ocr2key.UnmarshalMultichainPublicKey(onchainPublicKey); err != nil { + return nil, fmt.Errorf("the signer's onchain public key is not the encoded form a configuration lists: %w", err) + } + + k := &remoteKeyring{ + client: client, + onchainPublicKey: onchainPublicKey, + 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. +// +// 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 +// 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 { + key, err := ocr2key.OnchainPublicKeyFor(EVMFamily, publicKey) + if err != nil { + return false + } + return ocr2key.EvmVerifyBlob(key, ocr2key.ReportToSigData(ocr2key.OCR3ReportContext(digest, seqNr), report), signature) +} + +// 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. + +// 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" + +// 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}) +} diff --git a/libs/standalone/ocr/signer_test.go b/libs/standalone/ocr/signer_test.go new file mode 100644 index 000000000..65dd7f05d --- /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") +} 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..d757fdc2b --- /dev/null +++ b/libs/standalone/protohelpers/protoc/server.go.tmpl @@ -0,0 +1,244 @@ +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 }} + + "google.golang.org/protobuf/reflect/protoreflect" + + "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{} + +{{- $file := . }} +{{- 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}}" + +// 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) + 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/standalone/protohelpers/ui/assets.go b/libs/standalone/protohelpers/ui/assets.go new file mode 100644 index 000000000..982c6e442 --- /dev/null +++ b/libs/standalone/protohelpers/ui/assets.go @@ -0,0 +1,95 @@ +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 + + // The arithmetic for the special value types, shared by both pages: the + // encoding has to match Go's big.Int and decimal.Decimal exactly, and two + // copies of that would be two things to keep right. Prepended to each page's + // own script rather than served separately, so it cannot load second. + //go:embed resources/values.js + valuesJS string + + //go:embed resources/request.js + requestJS string + + // The subscription sidebar and its tables. Its own file rather than more of + // request.js: it is driven by what arrives on a stream rather than by the form, + // so the only thing the two share is the page they are on. + //go:embed resources/subscriptions.js + subscriptionsJS 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", valuesJS+requestJS, "js") +} +func subscriptionsJSFileName() string { + return hashedName("cre-debug-subscriptions", subscriptionsJS, "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"` + // Subscriptions are the services whose methods register a trigger, and + // TriggerIDHeader is what the trigger ID travels in. Both come from the + // descriptors rather than the page working out which is which from a name. + Subscriptions []string `json:"subscriptions"` + TriggerIDHeader string `json:"triggerIdHeader"` + // Path is where the pages are mounted, so the form can link to the fan-out + // page - which is where a subscription's events are shown. + Path string `json:"prefix"` + // Special are the messages the form offers a number for instead of their + // fields, and where a response holds them. See special.go. + Special SpecialConfig `json:"special"` + }{ + Metadata: Fields(), + Prefix: HeaderPrefix, + Subscriptions: s.subscriptionServices(), + TriggerIDHeader: TriggerIDHeader, + Path: s.prefix, + Special: s.specialConfig(), + } + 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" + valuesJS + "\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..a9ab3a841 --- /dev/null +++ b/libs/standalone/protohelpers/ui/context.go @@ -0,0 +1,62 @@ +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) + }) +} + +// triggerIDFromContext is the trigger ID a registration named, or a fresh one. +func triggerIDFromContext(ctx context.Context) string { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.MD{} + } + return TriggerIDFromHeaders(md.Get) +} + +// 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 +} + +// PreservedHeaders is every header a request may carry that has to reach Invoke: +// the metadata, and the trigger ID a subscription is identified by. +// +// The form generator drops anything it was not told to keep, so a header missing +// from here is one the page can offer a box for and never send. +func PreservedHeaders() []string { + return append(HeaderNames(), TriggerIDHeader) +} 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/hub.go b/libs/standalone/protohelpers/ui/hub.go new file mode 100644 index 000000000..ae0fb335b --- /dev/null +++ b/libs/standalone/protohelpers/ui/hub.go @@ -0,0 +1,602 @@ +package ui + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "sort" + "sync" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" +) + +// A trigger is not a call, so it does not fit the request-and-response the rest +// of the page is: it is registered once and then delivers whatever it delivers, +// for as long as it is registered, to however many instances registered it. +// +// The Hub is what holds that. A subscription is keyed by its trigger ID, which is +// the identifier the registration carried, so registering the same trigger ID on +// another instance later joins the subscription already running rather than +// starting a second one - which is what makes "send to two instances now, a third +// in a minute" one table rather than two. +// +// Within a subscription an event is keyed by its own ID, and every instance that +// delivered it is a column. Instances are meant to agree; when they do not, that +// is the thing worth seeing, so the payloads are hashed and a row that carries +// more than one hash is marked rather than averaged away. + +const ( + // DefaultGrace is how long a subscription outlives the last reader watching + // it. Closing the window is how a subscription is closed, and a reload closes + // the window - so the two are told apart by waiting to see whether anyone + // comes back. + DefaultGrace = time.Minute + + // DefaultRing is how many events a subscription keeps. A reader that + // reattaches is sent them, so the table it left is the table it returns to. + DefaultRing = 200 + + // clientBuffer is how far behind a reader may fall before it is dropped. + // Dropping is safe: the browser reconnects and is sent the whole table, which + // is more correct than a reader that has silently missed a row. + clientBuffer = 64 +) + +// Hub holds every live subscription of the process, keyed by trigger ID. +// +// One Hub is shared by every instance, the same way the Fleet is: an embed run's +// instances are separate registries but one browser, so a subscription registered +// across four of them has to be one thing for the page to show it as one table. +type Hub struct { + grace time.Duration + ring int + + // ctx outlives the request that registered a trigger. A registration is not + // the request's to own: the request returns as soon as the trigger is + // registered, and the events arrive long afterwards. + ctx context.Context + cancel context.CancelFunc + + mu sync.Mutex + subs map[string]*subscription +} + +// NewHub builds an empty Hub with the default grace and ring. +func NewHub() *Hub { + ctx, cancel := context.WithCancel(context.Background()) + return &Hub{ + grace: DefaultGrace, + ring: DefaultRing, + ctx: ctx, + cancel: cancel, + subs: map[string]*subscription{}, + } +} + +// Close unregisters every subscription and stops reading them. +func (h *Hub) Close() error { + h.mu.Lock() + subs := make([]*subscription, 0, len(h.subs)) + for _, s := range h.subs { + subs = append(subs, s) + } + h.subs = map[string]*subscription{} + h.mu.Unlock() + + var errs []error + for _, s := range subs { + errs = append(errs, s.close()) + } + h.cancel() + return errors.Join(errs...) +} + +// registration is one instance joining one subscription. +type registration struct { + triggerID string + capabilityID string + // service is the real service the trigger belongs to, for the page to show. + service string + method string + + instance int + label string + trigger capabilities.TriggerExecutable + + metadata capabilities.RequestMetadata + payload *anypb.Any + + // eventType is the generated Go type of the trigger's streamed message, which + // is how a delivered event is read out of the Any it arrives in. + eventType reflect.Type +} + +// subscribe registers one instance's trigger, joining the subscription with this +// trigger ID or starting it. +func (h *Hub) subscribe(r registration) (*Status, error) { + s, err := h.subscription(r) + if err != nil { + return nil, err + } + + if err = s.attach(r); err != nil { + // A subscription nobody managed to attach to is not left behind: it would + // sit in the sidebar claiming to be watching something. + h.dropIfEmpty(s) + return nil, err + } + + status := s.status(false) + return &status, nil +} + +// subscription finds the one this registration belongs to, or starts it. +// +// A trigger ID names a subscription, so one that already exists has to be the +// same trigger: joining "the cron I started" with a different method would put +// two unrelated streams in one table. +func (h *Hub) subscription(r registration) (*subscription, error) { + h.mu.Lock() + defer h.mu.Unlock() + + if existing, ok := h.subs[r.triggerID]; ok { + if existing.capabilityID != r.capabilityID || existing.method != r.method { + return nil, userErrorf( + "trigger ID %s is already subscribed to %s/%s, so it cannot also subscribe to %s/%s - use a different trigger ID", + r.triggerID, existing.capabilityID, existing.method, r.capabilityID, r.method) + } + return existing, nil + } + + s := &subscription{ + hub: h, + triggerID: r.triggerID, + capabilityID: r.capabilityID, + service: r.service, + method: r.method, + eventType: r.eventType, + created: time.Now().UTC(), + attached: map[int]*attachment{}, + events: map[string]*Row{}, + clients: map[*client]struct{}{}, + } + // The grace clock starts now, not when the first reader leaves: a subscription + // nobody ever watches is one nobody is going to close. + s.startGrace() + + h.subs[r.triggerID] = s + return s, nil +} + +// dropIfEmpty removes a subscription nothing is attached to. +func (h *Hub) dropIfEmpty(s *subscription) { + s.mu.Lock() + empty := len(s.attached) == 0 + s.mu.Unlock() + if !empty { + return + } + + h.mu.Lock() + defer h.mu.Unlock() + if h.subs[s.triggerID] == s { + delete(h.subs, s.triggerID) + } +} + +// get is the subscription with this trigger ID. +func (h *Hub) get(triggerID string) (*subscription, error) { + h.mu.Lock() + defer h.mu.Unlock() + s, ok := h.subs[triggerID] + if !ok { + return nil, userErrorf("no subscription with trigger ID %s", triggerID) + } + return s, nil +} + +// List is every live subscription, newest last, for the page's sidebar. +func (h *Hub) List() []Status { + h.mu.Lock() + subs := make([]*subscription, 0, len(h.subs)) + for _, s := range h.subs { + subs = append(subs, s) + } + h.mu.Unlock() + + sort.Slice(subs, func(i, j int) bool { return subs[i].created.Before(subs[j].created) }) + + out := make([]Status, 0, len(subs)) + for _, s := range subs { + out = append(out, s.status(false)) + } + return out +} + +// Unsubscribe closes a subscription, or detaches the instances named. +func (h *Hub) Unsubscribe(triggerID string, instances []int) error { + s, err := h.get(triggerID) + if err != nil { + return err + } + + if len(instances) == 0 { + h.remove(s) + return s.close() + } + + var errs []error + for _, index := range instances { + errs = append(errs, s.detach(index)) + } + h.dropIfEmpty(s) + return errors.Join(errs...) +} + +// Ack forwards a reader's acknowledgement to every instance that delivered the +// event. +// +// The page acks rather than this doing it on delivery: a capability that redelivers +// what was not acknowledged is doing what it is meant to, and acknowledging an +// event the browser has not been shown would hide exactly the delivery being +// debugged. +func (h *Hub) Ack(triggerID, eventID string) error { + s, err := h.get(triggerID) + if err != nil { + return err + } + return s.ack(h.ctx, eventID) +} + +// remove takes a subscription out of the hub without closing it. +func (h *Hub) remove(s *subscription) { + h.mu.Lock() + defer h.mu.Unlock() + if h.subs[s.triggerID] == s { + delete(h.subs, s.triggerID) + } +} + +// subscription is one trigger ID: the instances registered under it, the events +// they delivered, and the readers watching. +type subscription struct { + hub *Hub + + triggerID string + capabilityID string + service string + method string + eventType reflect.Type + created time.Time + + mu sync.Mutex + attached map[int]*attachment + events map[string]*Row + order []string + clients map[*client]struct{} + graceTimer *time.Timer + closed bool +} + +// attachment is one instance's registration, kept because unregistering takes the +// request that registered. +type attachment struct { + instance int + label string + trigger capabilities.TriggerExecutable + request capabilities.TriggerRegistrationRequest + stop context.CancelFunc +} + +// attach registers the trigger on one instance and starts reading its events. +func (s *subscription) attach(r registration) error { + request := capabilities.TriggerRegistrationRequest{ + TriggerID: r.triggerID, + Metadata: r.metadata, + Method: r.method, + Payload: r.payload, + } + + s.mu.Lock() + if s.closed { + s.mu.Unlock() + return userErrorf("subscription %s has been closed", s.triggerID) + } + if _, taken := s.attached[r.instance]; taken { + s.mu.Unlock() + return userErrorf("%s is already subscribed to trigger ID %s", r.label, s.triggerID) + } + s.mu.Unlock() + + // Registered outside the lock: this reaches the capability, and holding the + // subscription's lock across it would stall every reader of every event while + // one instance registers. + ctx, stop := context.WithCancel(s.hub.ctx) + events, err := r.trigger.RegisterTrigger(ctx, request) + if err != nil { + stop() + return fromCapability(err) + } + + a := &attachment{instance: r.instance, label: r.label, trigger: r.trigger, request: request, stop: stop} + + s.mu.Lock() + if s.closed { + s.mu.Unlock() + stop() + return userErrorf("subscription %s has been closed", s.triggerID) + } + if _, taken := s.attached[r.instance]; taken { + // Lost a race with another registration of the same instance. Undone rather + // than left registered twice. + s.mu.Unlock() + stop() + _ = r.trigger.UnregisterTrigger(s.hub.ctx, request) + return userErrorf("%s is already subscribed to trigger ID %s", r.label, s.triggerID) + } + s.attached[r.instance] = a + s.mu.Unlock() + + go s.read(ctx, a, events) + s.broadcast(Message{Type: MessageAttached, TriggerID: s.triggerID, Status: ptr(s.status(false))}) + return nil +} + +// detach unregisters one instance and stops reading it. +func (s *subscription) detach(instance int) error { + s.mu.Lock() + a, ok := s.attached[instance] + if ok { + delete(s.attached, instance) + } + s.mu.Unlock() + + if !ok { + return userErrorf("instance %d is not subscribed to trigger ID %s", instance, s.triggerID) + } + + a.stop() + err := a.trigger.UnregisterTrigger(s.hub.ctx, a.request) + s.broadcast(Message{Type: MessageAttached, TriggerID: s.triggerID, Status: ptr(s.status(false))}) + if err != nil { + return fromCapability(err) + } + return nil +} + +// close unregisters every instance and tells every reader the subscription is over. +func (s *subscription) close() error { + s.mu.Lock() + if s.closed { + s.mu.Unlock() + return nil + } + s.closed = true + if s.graceTimer != nil { + s.graceTimer.Stop() + s.graceTimer = nil + } + attached := make([]*attachment, 0, len(s.attached)) + for _, a := range s.attached { + attached = append(attached, a) + } + s.attached = map[int]*attachment{} + s.mu.Unlock() + + var errs []error + for _, a := range attached { + a.stop() + if err := a.trigger.UnregisterTrigger(s.hub.ctx, a.request); err != nil { + errs = append(errs, fmt.Errorf("%s: %w", a.label, err)) + } + } + + s.broadcast(Message{Type: MessageClosed, TriggerID: s.triggerID, Status: ptr(s.status(true))}) + s.disconnectAll() + return errors.Join(errs...) +} + +// read forwards one instance's events until the channel closes or the attachment +// is stopped. +func (s *subscription) read(ctx context.Context, a *attachment, events <-chan capabilities.TriggerResponse) { + for { + select { + case <-ctx.Done(): + return + case response, open := <-events: + if !open { + return + } + s.record(a, response) + } + } +} + +// record merges one instance's delivery of one event into its row. +func (s *subscription) record(a *attachment, response capabilities.TriggerResponse) { + node := Node{ + Instance: a.instance, + Label: a.label, + At: time.Now().UTC(), + } + if response.Err != nil { + node.Error = response.Err.Error() + } + + payload, id, err := s.decode(response.Event) + if err != nil && node.Error == "" { + node.Error = err.Error() + } + if err == nil { + node.PayloadID = id + } + + // An event with no ID cannot be a row of its own without every delivery + // looking like a separate event, so it is keyed by what it carried instead. + key := response.Event.ID + if key == "" { + key = "(no event ID) " + id + } + + row := s.merge(key, node, payload) + if row != nil { + s.broadcast(Message{Type: MessageRow, TriggerID: s.triggerID, Row: row}) + } +} + +// decode reads a delivered event out of the Any it arrives in, as the generated Go +// type the trigger declares, and hashes it. +// +// The hash is what makes disagreement visible: identical payloads hash the same, +// so a row with one hash is every instance agreeing and a row with two is not. +// Marshalling is deterministic so that a map field cannot make two equal payloads +// look different. +func (s *subscription) decode(event capabilities.TriggerEvent) (json.RawMessage, string, error) { + if event.Payload == nil { + if event.Outputs != nil { + // The values.Map path, which only a DAG registration takes. This + // package always registers with a payload, so seeing one means the + // capability answered a request it was not sent. + return nil, "", fmt.Errorf("the event carried Outputs rather than a Payload") + } + return nil, "", fmt.Errorf("the event carried no payload") + } + + message, ok := reflect.New(s.eventType.Elem()).Interface().(proto.Message) + if !ok { + return nil, "", fmt.Errorf("%s is not a protobuf message", s.eventType) + } + if err := event.Payload.UnmarshalTo(message); err != nil { + return nil, "", fmt.Errorf("failed to read the event as %T: %w", message, err) + } + + // Proto names, which is the spelling the response tab already uses, so an + // event and a response read the same way. + encoded, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(message) + if err != nil { + return nil, "", fmt.Errorf("failed to encode the event: %w", err) + } + + canonical, err := proto.MarshalOptions{Deterministic: true}.Marshal(message) + if err != nil { + return nil, "", fmt.Errorf("failed to hash the event: %w", err) + } + return encoded, shortHash(canonical), nil +} + +// merge folds a node into its row and returns the row to send, or nil if the +// subscription is closed. +func (s *subscription) merge(id string, node Node, payload json.RawMessage) *Row { + s.mu.Lock() + defer s.mu.Unlock() + + if s.closed { + return nil + } + + row, ok := s.events[id] + if !ok { + row = &Row{ID: id, First: node.At} + s.events[id] = row + s.order = append(s.order, id) + s.trim() + } + + row.add(node, payload) + + copied := row.clone() + return &copied +} + +// trim keeps the kept-event count at the ring size. Called with the lock held. +func (s *subscription) trim() { + for len(s.order) > s.hub.ring { + delete(s.events, s.order[0]) + s.order = s.order[1:] + } +} + +// rows is every kept event, oldest first. Copied, so a reader is never handed +// something another delivery is about to change under it. +func (s *subscription) rows() []Row { + s.mu.Lock() + defer s.mu.Unlock() + + out := make([]Row, 0, len(s.order)) + for _, id := range s.order { + if row, ok := s.events[id]; ok { + out = append(out, row.clone()) + } + } + return out +} + +// status describes the subscription, with its events when withRows. +func (s *subscription) status(closed bool) Status { + s.mu.Lock() + instances := make([]Attached, 0, len(s.attached)) + for _, a := range s.attached { + instances = append(instances, Attached{Instance: a.instance, Label: a.label}) + } + events := len(s.order) + readers := len(s.clients) + graced := s.graceTimer != nil + if s.closed { + closed = true + } + s.mu.Unlock() + + sort.Slice(instances, func(i, j int) bool { return instances[i].Instance < instances[j].Instance }) + + return Status{ + TriggerID: s.triggerID, + CapabilityID: s.capabilityID, + Service: s.service, + Method: s.method, + Instances: instances, + Events: events, + Readers: readers, + InGrace: graced && readers == 0, + Closed: closed, + Created: s.created, + } +} + +// ack forwards an acknowledgement to every instance that delivered the event. +func (s *subscription) ack(ctx context.Context, eventID string) error { + s.mu.Lock() + row, ok := s.events[eventID] + var targets []*attachment + if ok { + for _, node := range row.Nodes { + if a, attached := s.attached[node.Instance]; attached { + targets = append(targets, a) + } + } + } + s.mu.Unlock() + + if !ok { + return userErrorf("trigger ID %s has no event %s", s.triggerID, eventID) + } + + var errs []error + for _, a := range targets { + if err := a.trigger.AckEvent(ctx, s.triggerID, eventID, s.method); err != nil { + errs = append(errs, fmt.Errorf("%s: %w", a.label, err)) + } + } + if err := errors.Join(errs...); err != nil { + return fromCapability(err) + } + return nil +} + +func ptr[T any](v T) *T { return &v } diff --git a/libs/standalone/protohelpers/ui/hub_test.go b/libs/standalone/protohelpers/ui/hub_test.go new file mode 100644 index 000000000..dcf18cd3f --- /dev/null +++ b/libs/standalone/protohelpers/ui/hub_test.go @@ -0,0 +1,574 @@ +package ui + +import ( + "context" + "fmt" + "reflect" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "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" +) + +// The trigger the tests subscribe to is Executable.Execute, which is the +// streaming method of the same real service the rest of these tests use. So its +// event is a CapabilityResponse, whose Error field is a convenient thing to make +// two instances disagree about. +func eventType() reflect.Type { return reflect.TypeFor[*pb.CapabilityResponse]() } + +func event(t *testing.T, id, payload string) capabilities.TriggerResponse { + t.Helper() + wrapped, err := anypb.New(&pb.CapabilityResponse{Error: payload}) + require.NoError(t, err) + return capabilities.TriggerResponse{Event: capabilities.TriggerEvent{ID: id, Payload: wrapped}} +} + +// fakeTrigger is one instance's trigger capability, whose events the test +// delivers by hand. +// +// A channel per registration rather than one for the capability: the point of +// most of these tests is several instances delivering the same event, which is +// several registrations. +type fakeTrigger struct { + registerErr error + + mu sync.Mutex + channels []chan capabilities.TriggerResponse + registered []capabilities.TriggerRegistrationRequest + unregistered []capabilities.TriggerRegistrationRequest + acked []string +} + +func (f *fakeTrigger) Info(context.Context) (capabilities.CapabilityInfo, error) { + return capabilities.NewCapabilityInfo(testCapabilityID, capabilities.CapabilityTypeTrigger, "a trigger for tests") +} + +func (f *fakeTrigger) RegisterTrigger(_ context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + if f.registerErr != nil { + return nil, f.registerErr + } + + f.mu.Lock() + defer f.mu.Unlock() + ch := make(chan capabilities.TriggerResponse, 16) + f.channels = append(f.channels, ch) + f.registered = append(f.registered, request) + return ch, nil +} + +func (f *fakeTrigger) UnregisterTrigger(_ context.Context, request capabilities.TriggerRegistrationRequest) error { + f.mu.Lock() + defer f.mu.Unlock() + f.unregistered = append(f.unregistered, request) + return nil +} + +func (f *fakeTrigger) AckEvent(_ context.Context, triggerID, eventID, method string) error { + f.mu.Lock() + defer f.mu.Unlock() + f.acked = append(f.acked, fmt.Sprintf("%s/%s/%s", triggerID, eventID, method)) + return nil +} + +// deliver sends an event down the nth registration this capability handed out. +func (f *fakeTrigger) deliver(t *testing.T, n int, response capabilities.TriggerResponse) { + t.Helper() + f.mu.Lock() + require.Greater(t, len(f.channels), n, "registration %d was never made", n) + ch := f.channels[n] + f.mu.Unlock() + ch <- response +} + +func (f *fakeTrigger) requests() []capabilities.TriggerRegistrationRequest { + f.mu.Lock() + defer f.mu.Unlock() + return append([]capabilities.TriggerRegistrationRequest(nil), f.registered...) +} + +func (f *fakeTrigger) unregisters() []capabilities.TriggerRegistrationRequest { + f.mu.Lock() + defer f.mu.Unlock() + return append([]capabilities.TriggerRegistrationRequest(nil), f.unregistered...) +} + +func (f *fakeTrigger) acks() []string { + f.mu.Lock() + defer f.mu.Unlock() + return append([]string(nil), f.acked...) +} + +// join subscribes one instance to a trigger ID. +func join(t *testing.T, h *Hub, triggerID string, instance int, trigger capabilities.TriggerExecutable) (*Status, error) { + t.Helper() + payload, err := anypb.New(&pb.CapabilityRequest{}) + require.NoError(t, err) + + return h.subscribe(registration{ + triggerID: triggerID, + capabilityID: testCapabilityID, + service: string(testService().FullName()), + method: "Execute", + instance: instance, + label: fmt.Sprintf("instance %d", instance+1), + trigger: trigger, + payload: payload, + eventType: eventType(), + }) +} + +// eventually waits for a condition the hub reaches on a goroutine of its own. +func eventually(t *testing.T, why string, condition func() bool) { + t.Helper() + require.Eventually(t, condition, 2*time.Second, time.Millisecond, why) +} + +// A subscription registers the trigger with the ID and method it was asked for. +func TestSubscribeRegistersTheTrigger(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + status, err := join(t, h, "ui-trigger-abc", 0, trigger) + require.NoError(t, err) + + assert.Equal(t, "ui-trigger-abc", status.TriggerID) + assert.Equal(t, "Execute", status.Method) + assert.Equal(t, testCapabilityID, status.CapabilityID) + require.Len(t, status.Instances, 1) + assert.Equal(t, "instance 1", status.Instances[0].Label) + + requests := trigger.requests() + require.Len(t, requests, 1) + assert.Equal(t, "ui-trigger-abc", requests[0].TriggerID) + assert.Equal(t, "Execute", requests[0].Method) + assert.NotNil(t, requests[0].Payload, "the form's input is what the trigger is configured with") +} + +// The trigger ID is the subscription, so a second instance registering the same +// one joins it rather than starting another. This is what makes "two instances +// now, a third later" one table. +func TestSubscribingTheSameTriggerIDJoins(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + first, second := &fakeTrigger{}, &fakeTrigger{} + _, err := join(t, h, "ui-trigger-shared", 0, first) + require.NoError(t, err) + + status, err := join(t, h, "ui-trigger-shared", 3, second) + require.NoError(t, err) + + require.Len(t, status.Instances, 2) + assert.Equal(t, 0, status.Instances[0].Instance) + assert.Equal(t, 3, status.Instances[1].Instance) + assert.Len(t, h.List(), 1, "one subscription, not one per instance") +} + +// A trigger ID already watching something else would put two unrelated streams in +// one table, so it is refused - and refused as the caller's mistake, since a +// different ID fixes it. +func TestSubscribingTheSameTriggerIDToAnotherMethodIsRefused(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + _, err := join(t, h, "ui-trigger-taken", 0, &fakeTrigger{}) + require.NoError(t, err) + + payload, err := anypb.New(&pb.CapabilityRequest{}) + require.NoError(t, err) + _, err = h.subscribe(registration{ + triggerID: "ui-trigger-taken", + capabilityID: testCapabilityID, + method: "SomethingElse", + instance: 1, + trigger: &fakeTrigger{}, + payload: payload, + eventType: eventType(), + }) + require.Error(t, err) + assert.True(t, isUserError(err), "%v", err) + assert.Contains(t, err.Error(), "already subscribed") +} + +// Registering one instance twice under one trigger ID would register it twice in +// the capability, so it is refused rather than done. +func TestSubscribingOneInstanceTwiceIsRefused(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + _, err := join(t, h, "ui-trigger-dup", 2, trigger) + require.NoError(t, err) + + _, err = join(t, h, "ui-trigger-dup", 2, trigger) + require.Error(t, err) + assert.True(t, isUserError(err), "%v", err) + assert.Len(t, trigger.requests(), 1, "the second attempt must not reach the capability") +} + +// A registration the capability refuses is not left behind as a subscription +// nothing is attached to. +func TestFailedRegistrationLeavesNoSubscription(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + _, err := join(t, h, "ui-trigger-bad", 0, &fakeTrigger{registerErr: fmt.Errorf("no such schedule")}) + require.Error(t, err) + assert.Empty(t, h.List()) +} + +// Two instances delivering the same event is one row with a column each, and one +// payload hash: they agree. +func TestAgreeingInstancesAreOneRow(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + first, second := &fakeTrigger{}, &fakeTrigger{} + _, err := join(t, h, "ui-trigger-agree", 0, first) + require.NoError(t, err) + _, err = join(t, h, "ui-trigger-agree", 1, second) + require.NoError(t, err) + + s, err := h.get("ui-trigger-agree") + require.NoError(t, err) + + first.deliver(t, 0, event(t, "event-1", "same")) + second.deliver(t, 0, event(t, "event-1", "same")) + + eventually(t, "both instances should appear in the row", func() bool { + rows := s.rows() + return len(rows) == 1 && len(rows[0].Nodes) == 2 + }) + + rows := s.rows() + require.Len(t, rows, 1) + assert.Equal(t, "event-1", rows[0].ID) + assert.Len(t, rows[0].PayloadIDs, 1, "identical payloads hash the same") + assert.False(t, rows[0].Diverged) + + // One payload, held once on the row rather than repeated per instance, and both + // nodes point at it. + require.Len(t, rows[0].Payloads, 1) + assert.JSONEq(t, `{"error":"same"}`, string(rows[0].Payloads[0])) + assert.Equal(t, 0, rows[0].Nodes[0].PayloadIndex) + assert.Equal(t, 0, rows[0].Nodes[1].PayloadIndex) +} + +// The payloads and their hashes are in the same order, and a node says which of +// them it sent - which is what lets the table put a payload per column and have +// the instance row point into it. +func TestPayloadsAndHashesLineUp(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + first, second, third := &fakeTrigger{}, &fakeTrigger{}, &fakeTrigger{} + for i, trigger := range []*fakeTrigger{first, second, third} { + _, err := join(t, h, "ui-trigger-lineup", i, trigger) + require.NoError(t, err) + } + + s, err := h.get("ui-trigger-lineup") + require.NoError(t, err) + + // Instances 1 and 3 agree, instance 2 does not. Delivered in instance order so + // the arrival order is the one the row is expected to keep. + first.deliver(t, 0, event(t, "event-1", "agreed")) + eventually(t, "the first instance should be recorded", func() bool { + return len(s.rows()) == 1 && len(s.rows()[0].Nodes) == 1 + }) + second.deliver(t, 0, event(t, "event-1", "different")) + eventually(t, "the second instance should be recorded", func() bool { + return len(s.rows()[0].Nodes) == 2 + }) + third.deliver(t, 0, event(t, "event-1", "agreed")) + eventually(t, "the third instance should be recorded", func() bool { + return len(s.rows()[0].Nodes) == 3 + }) + + row := s.rows()[0] + assert.True(t, row.Diverged) + + // Two distinct payloads, in the order they arrived, and as many hashes as + // payloads. + require.Len(t, row.Payloads, 2) + require.Len(t, row.PayloadIDs, 2) + assert.JSONEq(t, `{"error":"agreed"}`, string(row.Payloads[0])) + assert.JSONEq(t, `{"error":"different"}`, string(row.Payloads[1])) + + // And each instance points at the one it sent. + assert.Equal(t, 0, row.Nodes[0].PayloadIndex) + assert.Equal(t, 1, row.Nodes[1].PayloadIndex) + assert.Equal(t, 0, row.Nodes[2].PayloadIndex, "the third instance agreed with the first") + + // The hash at an index is the hash of the payload at that index. + for i, node := range row.Nodes { + assert.Equal(t, row.PayloadIDs[node.PayloadIndex], node.PayloadID, "node %d", i) + } +} + +// An instance that failed has no payload to point at, so it points at nothing +// rather than at payload zero - which would read as agreeing with it. +func TestAFailedDeliveryPointsAtNoPayload(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + _, err := join(t, h, "ui-trigger-failed", 0, trigger) + require.NoError(t, err) + + s, err := h.get("ui-trigger-failed") + require.NoError(t, err) + + trigger.deliver(t, 0, capabilities.TriggerResponse{ + Event: capabilities.TriggerEvent{ID: "event-1"}, + Err: fmt.Errorf("the schedule was withdrawn"), + }) + eventually(t, "the failure should be recorded", func() bool { return len(s.rows()) == 1 }) + + row := s.rows()[0] + require.Len(t, row.Nodes, 1) + assert.Equal(t, -1, row.Nodes[0].PayloadIndex) + assert.Contains(t, row.Nodes[0].Error, "the schedule was withdrawn") + assert.Empty(t, row.Payloads) + assert.False(t, row.Diverged, "one instance failing is not two instances disagreeing") +} + +// Instances disagreeing is the bug the table exists to show, so the row says so +// rather than showing one of the two. +func TestDisagreeingInstancesDiverge(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + first, second := &fakeTrigger{}, &fakeTrigger{} + _, err := join(t, h, "ui-trigger-differ", 0, first) + require.NoError(t, err) + _, err = join(t, h, "ui-trigger-differ", 1, second) + require.NoError(t, err) + + s, err := h.get("ui-trigger-differ") + require.NoError(t, err) + + first.deliver(t, 0, event(t, "event-1", "this")) + second.deliver(t, 0, event(t, "event-1", "that")) + + eventually(t, "both instances should appear in the row", func() bool { + rows := s.rows() + return len(rows) == 1 && len(rows[0].Nodes) == 2 + }) + + rows := s.rows() + assert.True(t, rows[0].Diverged) + assert.Len(t, rows[0].PayloadIDs, 2) + assert.NotEqual(t, rows[0].Nodes[0].PayloadID, rows[0].Nodes[1].PayloadID) +} + +// The kept-event count is bounded, so a trigger firing all day does not grow +// without limit. +func TestEventsAreTrimmedToTheRing(t *testing.T) { + h := NewHub() + h.ring = 3 + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + _, err := join(t, h, "ui-trigger-ring", 0, trigger) + require.NoError(t, err) + + s, err := h.get("ui-trigger-ring") + require.NoError(t, err) + + for i := range 6 { + trigger.deliver(t, 0, event(t, fmt.Sprintf("event-%d", i), "payload")) + } + + eventually(t, "the oldest events should be dropped", func() bool { + rows := s.rows() + return len(rows) == 3 && rows[0].ID == "event-3" + }) +} + +// Nobody watching means nobody wants it, once the grace period has passed: this +// is what closing the window does. +func TestAbandonedSubscriptionsAreUnregistered(t *testing.T) { + h := NewHub() + h.grace = 10 * time.Millisecond + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + _, err := join(t, h, "ui-trigger-abandoned", 0, trigger) + require.NoError(t, err) + + eventually(t, "the trigger should be unregistered", func() bool { + return len(trigger.unregisters()) == 1 + }) + assert.Empty(t, h.List()) +} + +// A reader arriving inside the grace period is somebody coming back, which is +// what a reload looks like - so the subscription is still there. +func TestAReaderCancelsTheGracePeriod(t *testing.T) { + h := NewHub() + h.grace = 50 * time.Millisecond + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + _, err := join(t, h, "ui-trigger-kept", 0, trigger) + require.NoError(t, err) + + s, err := h.get("ui-trigger-kept") + require.NoError(t, err) + + c := newClient() + s.addClient(c) + + time.Sleep(150 * time.Millisecond) + assert.Empty(t, trigger.unregisters(), "a watched subscription must not be unregistered") + assert.Len(t, h.List(), 1) + + // And leaving starts the clock again. + s.removeClient(c) + eventually(t, "the trigger should be unregistered once the reader leaves", func() bool { + return len(trigger.unregisters()) == 1 + }) +} + +// A reader that reattaches is sent the table it left, which is what makes a +// reload look like nothing happened. +func TestASnapshotCarriesTheTable(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + _, err := join(t, h, "ui-trigger-snapshot", 0, trigger) + require.NoError(t, err) + + s, err := h.get("ui-trigger-snapshot") + require.NoError(t, err) + + trigger.deliver(t, 0, event(t, "event-1", "first")) + trigger.deliver(t, 0, event(t, "event-2", "second")) + eventually(t, "both events should be recorded", func() bool { return len(s.rows()) == 2 }) + + snapshot := s.addClient(newClient()) + require.Len(t, snapshot.Rows, 2) + assert.Equal(t, "event-1", snapshot.Rows[0].ID) + assert.Equal(t, 1, snapshot.Readers) +} + +// A reader watching is sent each event as it arrives. +func TestReadersAreSentRows(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + trigger := &fakeTrigger{} + _, err := join(t, h, "ui-trigger-live", 0, trigger) + require.NoError(t, err) + + s, err := h.get("ui-trigger-live") + require.NoError(t, err) + + c := newClient() + s.addClient(c) + + trigger.deliver(t, 0, event(t, "event-1", "hello")) + + select { + case m := <-c.messages: + assert.Equal(t, MessageRow, m.Type) + require.NotNil(t, m.Row) + assert.Equal(t, "event-1", m.Row.ID) + case <-time.After(2 * time.Second): + t.Fatal("the reader was sent nothing") + } +} + +// Acknowledging goes to the instances that delivered the event, and carries the +// method, because that is what a capability keys its delivery on. +func TestAckReachesTheInstancesThatDelivered(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + first, second := &fakeTrigger{}, &fakeTrigger{} + _, err := join(t, h, "ui-trigger-ack", 0, first) + require.NoError(t, err) + _, err = join(t, h, "ui-trigger-ack", 1, second) + require.NoError(t, err) + + s, err := h.get("ui-trigger-ack") + require.NoError(t, err) + + // Only the first instance delivers it. + first.deliver(t, 0, event(t, "event-1", "hello")) + eventually(t, "the event should be recorded", func() bool { return len(s.rows()) == 1 }) + + require.NoError(t, h.Ack("ui-trigger-ack", "event-1")) + assert.Equal(t, []string{"ui-trigger-ack/event-1/Execute"}, first.acks()) + assert.Empty(t, second.acks(), "an instance that never delivered it has nothing to acknowledge") +} + +// Acking something that was never delivered is the caller's mistake, not a broken +// page. +func TestAckOfAnUnknownEventIsAUserError(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + _, err := join(t, h, "ui-trigger-noack", 0, &fakeTrigger{}) + require.NoError(t, err) + + err = h.Ack("ui-trigger-noack", "never-happened") + require.Error(t, err) + assert.True(t, isUserError(err), "%v", err) +} + +// Closing a subscription unregisters every instance and takes it out of the list. +func TestUnsubscribeClosesEverything(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + first, second := &fakeTrigger{}, &fakeTrigger{} + _, err := join(t, h, "ui-trigger-close", 0, first) + require.NoError(t, err) + _, err = join(t, h, "ui-trigger-close", 1, second) + require.NoError(t, err) + + require.NoError(t, h.Unsubscribe("ui-trigger-close", nil)) + assert.Len(t, first.unregisters(), 1) + assert.Len(t, second.unregisters(), 1) + assert.Empty(t, h.List()) +} + +// Detaching one instance leaves the rest of the subscription running, which is how +// "stop instance 3 and watch the others" works. +func TestUnsubscribeCanDetachOneInstance(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + first, second := &fakeTrigger{}, &fakeTrigger{} + _, err := join(t, h, "ui-trigger-partial", 0, first) + require.NoError(t, err) + _, err = join(t, h, "ui-trigger-partial", 1, second) + require.NoError(t, err) + + require.NoError(t, h.Unsubscribe("ui-trigger-partial", []int{1})) + assert.Empty(t, first.unregisters()) + assert.Len(t, second.unregisters(), 1) + + require.Len(t, h.List(), 1) + assert.Len(t, h.List()[0].Instances, 1) +} + +// A subscription that was never opened is not something the page can watch. +func TestStreamingAnUnknownTriggerIDIsAUserError(t *testing.T) { + h := NewHub() + t.Cleanup(func() { _ = h.Close() }) + + _, err := h.get("ui-trigger-nothing") + require.Error(t, err) + assert.True(t, isUserError(err), "%v", err) +} 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..a93a6dda8 --- /dev/null +++ b/libs/standalone/protohelpers/ui/mount.go @@ -0,0 +1,488 @@ +package ui + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "html/template" + "io" + "net/http" + "path" + "sort" + "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" + +// Options is what mounting one instance's debug page needs. +type Options struct { + // Mux is what the pages are served on: the instance's own HTTP server, so a + // browser on any instance's port can drive the whole process. + Mux *http.ServeMux + // Prefix roots both pages. Empty means DefaultPrefix. + Prefix string + // Server is this instance's capabilities. + Server *Server + // Fleet is every instance's page, so the fan-out can reach a sibling. + Fleet *Fleet + // Hub holds the subscriptions. Shared with every other instance, so a trigger + // registered across several of them is one subscription with a column each. + Hub *Hub + // Index is this instance's number, which names it on the fan-out page and on + // every event it delivers. + Index int + // Title is what the per-instance page calls itself. + Title string +} + +// Mount serves this instance's debug page, and adds it to the 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(o Options) error { + if o.Mux == nil { + return fmt.Errorf("a mux is required") + } + if o.Server == nil { + return fmt.Errorf("a server is required") + } + if o.Fleet == nil { + return fmt.Errorf("a fleet is required") + } + if o.Hub == nil { + return fmt.Errorf("a subscription hub is required") + } + + prefix := o.Prefix + if prefix == "" { + prefix = DefaultPrefix + } + prefix = "/" + strings.Trim(prefix, "/") + + mux, s := o.Mux, o.Server + label := fmt.Sprintf("instance %d", o.Index+1) + + // Which instance this is, and where its subscriptions go. Set here rather than + // passed to New because this is where an instance's identity is known: New is + // given capabilities, and a capability does not know which instance is hosting + // it. + s.hub = o.Hub + s.index = o.Index + s.label = label + s.prefix = prefix + + served, err := customJS(s) + if err != nil { + return err + } + + page := standalone.Handler( + s, + o.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, and so + // does the trigger ID a subscription is identified by, so every header + // either travels in has to reach Invoke. + standalone.PreserveHeaders(PreservedHeaders()), + ) + + uiPath := prefix + "/ui" + mux.Handle(uiPath+"/", http.StripPrefix(uiPath, page)) + + o.Fleet.Add(&Instance{ + Index: o.Index, + Label: label, + handler: page, + }) + + f := &fanout{fleet: o.Fleet, hub: o.Hub, 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) + + // The subscriptions: what is running, what they have delivered, and the two + // things a reader does about it. + mux.HandleFunc(prefix+"/request/subscriptions", f.subscriptions) + mux.HandleFunc(prefix+"/request/subscriptions/stream", f.stream) + mux.HandleFunc(prefix+"/request/subscriptions/ack", f.ack) + mux.HandleFunc(prefix+"/request/subscriptions/close", f.unsubscribe) + mux.HandleFunc(prefix+"/request/trigger-id", f.triggerID) + + // 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 + hub *Hub + 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"` + + // Subscriptions are the services whose methods register a trigger rather than + // calling it. Invoking one opens a subscription instead of returning a + // response, so the page has to know which it is looking at. + Subscriptions []string `json:"subscriptions"` + // TriggerIDHeader is what the trigger ID travels in, so the page does not + // repeat the name. + TriggerIDHeader string `json:"triggerIdHeader"` + // Special are the messages shown as the number they stand for rather than as + // the fields they are made of, and where each method's response holds them. + // See special.go. + Special SpecialConfig `json:"special"` +} + +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) + } + for _, methods := range services { + sort.Strings(methods) + } + + return pageConfig{ + Instances: f.fleet.List(), + Services: services, + UIPath: f.uiPath, + Prefix: f.prefix, + Metadata: Fields(), + Subscriptions: f.server.subscriptionServices(), + TriggerIDHeader: TriggerIDHeader, + Special: f.server.specialConfig(), + } +} + +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 + SubscriptionsFile string + UIPath string + Prefix string + Config template.JS + }{ + CSSFile: cssFileName(), + JSFile: requestJSFileName(), + SubscriptionsFile: subscriptionsJSFileName(), + 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") + // The shared arithmetic first, so the page's own script can rely on it. + _, _ = io.WriteString(w, valuesJS+"\n"+requestJS) + case subscriptionsJSFileName(): + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") + _, _ = io.WriteString(w, subscriptionsJS) + 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 instance's answer. 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"` + // ResponseID is the hash of what this instance answered, and ResponseIndex is + // which of the fan-out's distinct responses that is. + // + // The response itself is on the fan-out rather than here, for the same reason a + // trigger event's payload is on its row: instances answering identically is the + // normal case, and holding it per instance would repeat the same JSON once per + // instance to say they matched. + ResponseID string `json:"responseId,omitempty"` + ResponseIndex int `json:"responseIndex"` + Error string `json:"error,omitempty"` +} + +type fanoutResponse struct { + Method string `json:"method"` + Results []instanceResult `json:"results"` + // TriggerID is the subscription every group was registered under, for a + // fan-out that subscribed rather than called. Reported back because the page + // needs it to open the stream, and because a caller that named none still has + // to be told which one it got. + TriggerID string `json:"triggerId,omitempty"` + + // The distinct responses, and whether the instances disagreed. Same shape as a + // trigger event's row, because it is the same question: what did each instance + // say, and did they all say it. + payloadSet +} + +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. + get := func(name string) []string { return req.Metadata[name] } + + metadata, err := MetadataFromHeaders(get) + if err != nil { + writeError(w, err) + return + } + header := HeadersFromMetadata(metadata) + + // Settled here for the same reason, and it matters more: the trigger ID is + // what identifies a subscription, so instances left to mint their own would + // each start a subscription of their own and the one table the user asked for + // would be four. + triggerID := TriggerIDFromHeaders(get) + header.Set(TriggerIDHeader, triggerID) + + response := f.run(req, header) + response.TriggerID = triggerID + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); 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}) + } + } + + type answer struct { + result instanceResult + response json.RawMessage + } + + answers := make(map[int]answer, 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() + response = nil + } + + mu.Lock() + defer mu.Unlock() + answers[j.instance.Index] = answer{result: row, response: response} + }(j) + } + wg.Wait() + + // Collected in instance order rather than as they arrived, so the responses are + // numbered the same way twice in a row for the same fan-out. Concurrency makes + // arrival order arbitrary, and a debug page that renumbers its columns between + // two identical runs is a page that looks like it found something. + out := fanoutResponse{Method: req.Method, Results: make([]instanceResult, 0, len(all))} + for _, in := range all { + got, ok := answers[in.Index] + if !ok { + out.Results = append(out.Results, instanceResult{ + Instance: in.Index, + Label: in.Label, + Status: "na", + ResponseIndex: -1, + }) + continue + } + + // The hash is of the bytes the instance's page produced, which is what the + // form generator rendered and what is about to be shown. + row := got.result + if len(got.response) > 0 { + row.ResponseID = shortHash(got.response) + } + row.ResponseIndex = out.add(row.ResponseID, got.response) + out.Results = append(out.Results, row) + } + 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..dd2a25c97 --- /dev/null +++ b/libs/standalone/protohelpers/ui/resources/form.js @@ -0,0 +1,817 @@ +// 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: "", subscriptions: [], triggerIdHeader: "", prefix: "", + special: { bigInt: "", decimal: "", methods: {} } + }; + + // A method on one of these services registers a trigger rather than calling + // something, so it takes a trigger ID and its events turn up on the fan-out + // page rather than in the Response tab. + function isSubscribing() { + return (CONFIG.subscriptions || []).indexOf($("#grpc-service").val()) !== -1; + } + + // ---- 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($("