From 853e666cb99cc8b0f91b1738583bf206af76180f Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Tue, 29 Sep 2026 13:56:47 -0400 Subject: [PATCH 1/7] chain-write: emit cre:workflow:gas usage MeterRecords for EVM and Solana (CRE-6781, CRE-6782) When the host sets [Metering].CapabilityUsageEnabled (CL_CAPABILITY_USAGE_ENABLED in the LOOP env), the EVM and Solana plugins build a ResourceManager on the LOOP's durable emitter and emit one METER_ACTION_USAGE record per chain write at the point where the fee is already resolved for the legacy metering metadata: - resource type cre:workflow:gas:, value in wei / lamports (EmitUsageValue, no int64 truncation) - Utilization.ResourceId ":", EventId = tx hash (EVM) or tx signature (Solana): identical on every node that observes the write - Identity.Service chain-write, DON id = capability DON id when known Independent of MeterRecordsEnabled. Each emission logs "Emitted capability usage meter record" (executionID, eventID, resourceType, value, orgID, txHash), a contract for the billing reconciler, covered by unit tests in both plugins. Logs a warning at start-up if the LOOP has no durable emitter configured, since records would then be dropped fail-open. Co-Authored-By: Claude Fable 5.1 --- chain_capabilities/evm/actions/actions.go | 11 +++ .../evm/actions/gas_usage_test.go | 80 +++++++++++++++++++ .../evm/actions/write_report.go | 50 ++++++++++-- chain_capabilities/evm/go.mod | 6 +- chain_capabilities/evm/go.sum | 9 +-- chain_capabilities/evm/main.go | 40 +++++++++- chain_capabilities/solana/actions/actions.go | 11 +++ .../solana/actions/gas_usage_test.go | 80 +++++++++++++++++++ .../solana/actions/write_report.go | 39 +++++++++ chain_capabilities/solana/go.mod | 6 +- chain_capabilities/solana/go.sum | 4 +- chain_capabilities/solana/main.go | 35 +++++++- 12 files changed, 346 insertions(+), 25 deletions(-) create mode 100644 chain_capabilities/evm/actions/gas_usage_test.go create mode 100644 chain_capabilities/solana/actions/gas_usage_test.go diff --git a/chain_capabilities/evm/actions/actions.go b/chain_capabilities/evm/actions/actions.go index 2678c2b22..71df33fc2 100644 --- a/chain_capabilities/evm/actions/actions.go +++ b/chain_capabilities/evm/actions/actions.go @@ -21,6 +21,7 @@ import ( evmservice "github.com/smartcontractkit/chainlink-common/pkg/chains/evm" commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" @@ -44,6 +45,8 @@ type EVM struct { types.EVMService consensusHandler chainconsensus.RequestHandler chainSelector uint64 + usageMeter *resourcemanager.ResourceManager + usageIdentity resourcemanager.ResourceIdentity keystoneForwarderAddress common.Address forwarderClient contracts.CREForwarderClient ReceiverGasMinimum uint64 @@ -95,6 +98,14 @@ func NewEVM(cfg config.Config, evmService types.EVMService, lggr logger.Logger, return e, nil } +// WithUsageMeter enables cre:workflow:gas usage MeterRecords for write reports. +// identity is the base metering identity (DON id already stamped). Nil rm +// disables emission. +func (e *EVM) WithUsageMeter(rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity) { + e.usageMeter = rm + e.usageIdentity = identity +} + func (e *EVM) initLimiters(limitsFactory limits.Factory) (err error) { e.readPayloadSizeLimiter, err = limits.MakeUpperBoundLimiter(limitsFactory, cresettings.Default.PerWorkflow.ChainRead.PayloadSizeLimit) if err != nil { diff --git a/chain_capabilities/evm/actions/gas_usage_test.go b/chain_capabilities/evm/actions/gas_usage_test.go new file mode 100644 index 000000000..c0adad5fb --- /dev/null +++ b/chain_capabilities/evm/actions/gas_usage_test.go @@ -0,0 +1,80 @@ +package actions + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" + meteringpb "github.com/smartcontractkit/chainlink-protos/metering/go" +) + +type recordingEmitter struct{ records []*meteringpb.MeterRecord } + +func (r *recordingEmitter) Emit(_ context.Context, body []byte, _ ...any) error { + var rec meteringpb.MeterRecord + if err := proto.Unmarshal(body, &rec); err != nil { + return err + } + r.records = append(r.records, &rec) + return nil +} + +func TestEmitGasUsage(t *testing.T) { + metadata := capabilities.RequestMetadata{WorkflowID: "wf-1", WorkflowExecutionID: "exec-1", OrgID: "org-1"} + identity := resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite, ResourcePool: resourcemanager.WorkflowUsageResourcePool} + fee, _ := new(big.Int).SetString("123456789012345678901234", 10) + + t.Run("emits one gas record keyed by tx hash and logs the contract line", func(t *testing.T) { + lggr, obs := logger.TestObserved(t, zapcore.InfoLevel) + emitter := &recordingEmitter{} + rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) + + emitGasUsage(t.Context(), lggr, rm, identity, 421614, metadata, "0xabc", fee) + + require.Len(t, emitter.records, 1) + rec := emitter.records[0] + require.Equal(t, meteringpb.MeterAction_METER_ACTION_USAGE, rec.GetAction()) + require.Equal(t, resourcemanager.EmittingServiceChainWrite, rec.GetIdentity().GetService()) + require.Len(t, rec.GetUtilizations(), 1) + u := rec.GetUtilizations()[0] + require.Equal(t, "cre:workflow:gas:421614", u.GetResourceType()) + require.Equal(t, "wf-1:exec-1", u.GetResourceId()) + require.Equal(t, "0xabc", u.GetEventId()) + require.Equal(t, "org-1", u.GetOrgId()) + require.Equal(t, "123456789012345678901234", u.GetValue()) + + logs := obs.FilterMessage("Emitted capability usage meter record").All() + require.Len(t, logs, 1) + fields := logs[0].ContextMap() + require.Equal(t, "exec-1", fields["executionID"]) + require.Equal(t, "0xabc", fields["eventID"]) + require.Equal(t, "cre:workflow:gas:421614", fields["resourceType"]) + require.Equal(t, "123456789012345678901234", fields["value"]) + require.Equal(t, "org-1", fields["orgID"]) + }) + + t.Run("no-op without a meter or fee", func(t *testing.T) { + lggr := logger.Test(t) + emitter := &recordingEmitter{} + rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) + emitGasUsage(t.Context(), lggr, nil, identity, 1, metadata, "0xabc", fee) + emitGasUsage(t.Context(), lggr, rm, identity, 1, metadata, "0xabc", nil) + require.Empty(t, emitter.records) + }) + + t.Run("refuses a malformed resource id instead of emitting a bad record", func(t *testing.T) { + lggr, obs := logger.TestObserved(t, zapcore.ErrorLevel) + emitter := &recordingEmitter{} + rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) + emitGasUsage(t.Context(), lggr, rm, identity, 1, capabilities.RequestMetadata{WorkflowID: "", WorkflowExecutionID: "exec-1"}, "0xabc", fee) + require.Empty(t, emitter.records) + require.Len(t, obs.FilterMessage("Gas usage meter record not emitted").All(), 1) + }) +} diff --git a/chain_capabilities/evm/actions/write_report.go b/chain_capabilities/evm/actions/write_report.go index 61ab5a0bb..047e39c4d 100644 --- a/chain_capabilities/evm/actions/write_report.go +++ b/chain_capabilities/evm/actions/write_report.go @@ -21,6 +21,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" @@ -52,6 +53,9 @@ type WriteReport struct { writeReportL1FeeActive limits.RangeLimiter[commoncfg.Timestamp] transmissionScheduler ts.TransmissionScheduler executionTimestamp time.Time + + usageMeter *resourcemanager.ResourceManager + usageIdentity resourcemanager.ResourceIdentity } func (e *EVM) WriteReport(ctx context.Context, metadata capabilities.RequestMetadata, input *evm.WriteReportRequest) (*capabilities.ResponseAndMetadata[*evm.WriteReportReply], caperrors.Error) { @@ -97,6 +101,8 @@ func (e *EVM) executeWriteReport(ctx context.Context, request *evm.WriteReportRe writeReportL1FeeActive: e.writeReportL1FeeActive, transmissionScheduler: e.transmissionScheduler, executionTimestamp: metadata.ExecutionTimestamp, + usageMeter: e.usageMeter, + usageIdentity: e.usageIdentity, } return wr.executeWriteReport(ctx, request, metadata, telemetryContext) @@ -147,7 +153,7 @@ func (e *WriteReport) executeWriteReport(ctx context.Context, request *evm.Write if err != nil { return nil, capabilities.ResponseMetadata{}, err } - return reply, e.meteringFromReply(reply), nil + return reply, e.meteringFromReply(ctx, metadata, reply), nil case contracts.TransmissionStateInvalidReceiver: txHash, err := txHashRetriever.GetFailedTransmissionHash(ctx) if err != nil { @@ -166,7 +172,7 @@ func (e *WriteReport) executeWriteReport(ctx context.Context, request *evm.Write // though the receipt/fee lookup failed (e.g. flaky RPC), as a user error. return nil, capabilities.ResponseMetadata{}, revertReplyBuildError(transmissionInfo, transmissionID, err) } - return reply, e.meteringFromReply(reply), nil + return reply, e.meteringFromReply(ctx, metadata, reply), nil case contracts.TransmissionStateFailed: hadEnoughGas, calculatedReceiverGasBudget := e.attemptHadEnoughGas(request, transmissionInfo) if hadEnoughGas { @@ -191,7 +197,7 @@ func (e *WriteReport) executeWriteReport(ctx context.Context, request *evm.Write // reason even if the receipt/fee lookup failed, as a user error. return nil, capabilities.ResponseMetadata{}, revertReplyBuildError(transmissionInfo, transmissionID, err) } - return reply, e.meteringFromReply(reply), nil + return reply, e.meteringFromReply(ctx, metadata, reply), nil } monitoring.LogAndEmitSuccess(ctx, "Retrying a failed transmission after prior attempt had insufficient receiver gas", e.lggr, e.beholderProcessor, e.messageBuilder.BuildWriteReportInsufficientGasRetry(telemetryContext, request, calculatedReceiverGasBudget, transmissionInfo.GasLimit, queuePosition)) @@ -250,7 +256,7 @@ func (e *WriteReport) executeWriteReport(ctx context.Context, request *evm.Write if err != nil { return nil, capabilities.ResponseMetadata{}, err } - return reply, e.meteringFromReply(reply), nil + return reply, e.meteringFromReply(ctx, metadata, reply), nil case contracts.TransmissionStateFailed, contracts.TransmissionStateInvalidReceiver: txHash := &transactionResult.TxHash // if this is a re-attempt find the original failed tx hash @@ -274,7 +280,7 @@ func (e *WriteReport) executeWriteReport(ctx context.Context, request *evm.Write // user learns the cause even if the receipt/fee lookup failed, as a user error. return nil, capabilities.ResponseMetadata{}, revertReplyBuildError(newTransmissionInfo, transmissionID, err) } - return reply, e.meteringFromReply(reply), nil + return reply, e.meteringFromReply(ctx, metadata, reply), nil default: errorMsg := getInvalidStateErrorMessage(newTransmissionInfo.State) monitoring.LogAndEmitError(ctx, e.lggr, e.beholderProcessor, e.messageBuilder.BuildWriteReportInvalidTransmissionState(telemetryContext, request, newTransmissionInfo, fmt.Sprintf("WriteReport invalid transmission state with tx status: %d", transactionResult.TxStatus), errorMsg)) @@ -529,15 +535,47 @@ func (e *WriteReport) replyFromReceipt(ctx context.Context, txHash evmtypes.Hash // in the reply, so that every node reports the gas spent on chain, regardless of which node // transmitted. Deriving it from the reply guarantees the metered fee matches the reply's // TransactionFee. An absent fee yields empty metadata. -func (e *WriteReport) meteringFromReply(reply *evm.WriteReportReply) capabilities.ResponseMetadata { +func (e *WriteReport) meteringFromReply(ctx context.Context, metadata capabilities.RequestMetadata, reply *evm.WriteReportReply) capabilities.ResponseMetadata { feeInWei := pb.NewIntFromBigInt(reply.TransactionFee) if feeInWei == nil { e.lggr.Warnw("Transaction fee unavailable in reply; skipping metering", "txHash", hex.EncodeToString(reply.TxHash)) return capabilities.ResponseMetadata{} } + emitGasUsage(ctx, e.lggr, e.usageMeter, e.usageIdentity, e.chainSelector, metadata, hex.EncodeToString(reply.TxHash), feeInWei) return metering.GetResponseMetadataWriteReport(feeInWei, e.chainSelector) } +// emitGasUsage emits the cre:workflow:gas: usage MeterRecord for +// one chain write and logs the emission. The log line is a contract consumed by +// the billing reconciler (fields: executionID, eventID, resourceType, value, +// orgID, txHash) and must stay stable. Fail-open: never affects the reply. +func emitGasUsage(ctx context.Context, lggr logger.Logger, rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity, chainSelector uint64, metadata capabilities.RequestMetadata, txHash string, fee *big.Int) { + if rm == nil || fee == nil { + return + } + resourceID, err := resourcemanager.WorkflowUsageResourceID(metadata.WorkflowID, metadata.WorkflowExecutionID) + if err != nil { + lggr.Errorw("Gas usage meter record not emitted", "err", err, "executionID", metadata.WorkflowExecutionID) + return + } + resourceType := resourcemanager.WorkflowGasResourceType(chainSelector) + // The capability event id for gas is the transaction hash: one record per + // on-chain write, identical on every node of the DON that observes it. + rm.EmitUsageValue(ctx, identity, txHash, fee, resourcemanager.UtilizationFields{ + ResourceType: resourceType, + ResourceID: resourceID, + OrgID: metadata.OrgID, + }) + lggr.Infow("Emitted capability usage meter record", + "executionID", metadata.WorkflowExecutionID, + "eventID", txHash, + "resourceType", resourceType, + "value", fee.String(), + "orgID", metadata.OrgID, + "txHash", txHash, + ) +} + func (e *WriteReport) includeL1FeeInReceiptFee(ctx context.Context) bool { if e.writeReportL1FeeActive == nil { return false diff --git a/chain_capabilities/evm/go.mod b/chain_capabilities/evm/go.mod index eda71bd13..e9d4d0667 100644 --- a/chain_capabilities/evm/go.mod +++ b/chain_capabilities/evm/go.mod @@ -8,11 +8,12 @@ require ( github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/chain-selectors v1.0.111 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260917115705-1d3a14a9b049 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 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-20260821155228-fa1d775e2138 + github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe github.com/smartcontractkit/libocr v0.0.0-20260810200708-618b5bf7f342 github.com/stretchr/testify v1.12.1 go.opentelemetry.io/otel v1.46.0 @@ -50,7 +51,6 @@ require ( 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/dominikbraun/graph v0.23.0 // indirect github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect @@ -146,7 +146,6 @@ require ( github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260724153515-bb6a2de39bcb // indirect github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260723212800-b2f21d31b1d2 // indirect github.com/smartcontractkit/chainlink-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 @@ -207,7 +206,6 @@ require ( 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 - sigs.k8s.io/yaml v1.4.0 // indirect ) replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20250528121202-292529af39df diff --git a/chain_capabilities/evm/go.sum b/chain_capabilities/evm/go.sum index 49f5fd03f..8ed661092 100644 --- a/chain_capabilities/evm/go.sum +++ b/chain_capabilities/evm/go.sum @@ -91,8 +91,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= -github.com/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= @@ -190,7 +188,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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= @@ -461,8 +458,8 @@ github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866/go.mod h1:7q+YF5LlwVYqVPMLBgZ5gLQyCbgaOt3NsvD0gFfOhSM= github.com/smartcontractkit/chain-selectors v1.0.111 h1:urb/qOyRanxDn+Wras+LULHEHSlJ4tYnfeAHzeMBn5U= github.com/smartcontractkit/chain-selectors v1.0.111/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260917115705-1d3a14a9b049 h1:WebE9HTubitMC5xq+xCAArEI+oBLekVOoxYnCkB8whI= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260917115705-1d3a14a9b049/go.mod h1:3CaCxSyZmgWl1PhxneFSZqOfN7DF2VnHML0bTfiDNIo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 h1:VY3d0Au+a5PzJUHjuJ7vc00DrRL6Wd/QlbfSLZqzhwE= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049 h1:nQ2bw8QQLT+AuCE6JUVIRT1RAo08wQHR9jWh+MgT2GA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049/go.mod h1:FO9igruzwHPXstyb9J9z83sJ19iLSNSIg+0H/n0wlJU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= @@ -837,5 +834,3 @@ 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= diff --git a/chain_capabilities/evm/main.go b/chain_capabilities/evm/main.go index 2ba1c0873..16c8524ff 100644 --- a/chain_capabilities/evm/main.go +++ b/chain_capabilities/evm/main.go @@ -34,6 +34,7 @@ import ( 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/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" @@ -57,10 +58,17 @@ type capabilityGRPCService struct { capability lggr logger.Logger limitsFactory limits.Factory + + // meteringCfg and capabilityUsageEnabled come from the LOOP environment + // ([Metering] on the host). usageMeter emits cre:workflow:gas usage records + // for write reports when enabled; nil otherwise. + meteringCfg resourcemanager.Config + capabilityUsageEnabled bool } type capability struct { *actions.EVM + usageMeter *resourcemanager.ResourceManager id string requestPoller *poller.Poller consensusHandler chainconsensus.Handler @@ -73,7 +81,12 @@ var _ evmcapserver.ClientCapability = &capabilityGRPCService{} func main() { loopserver.ServeNew(CapabilityName, func(s *loop.Server) loop.StandardCapabilities { - return evmcapserver.NewClientServer(&capabilityGRPCService{lggr: s.Logger, limitsFactory: s.LimitsFactory}) + return evmcapserver.NewClientServer(&capabilityGRPCService{ + lggr: s.Logger, + limitsFactory: s.LimitsFactory, + meteringCfg: s.MeteringConfig(), + capabilityUsageEnabled: s.EnvConfig.CapabilityUsageEnabled, + }) }, loop.WithOtelViews(append(consMetrics.MetricViews(), monitoring.MetricViews()...))) } @@ -169,6 +182,22 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor if err != nil { return fmt.Errorf("failed to init evm relayer for chainID %d from relayer: %w", cfg.ChainID, err) } + if c.capabilityUsageEnabled { + // Gated by [Metering].CapabilityUsageEnabled on the host, independent of + // MeterRecordsEnabled (durable resource metering), so force records on. + rmCfg := c.meteringCfg.ResourceManagerConfig + rmCfg.MeterRecordsEnabled = true + rmCfg.MeterSnapshotsEnabled = false + c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) + identity := resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, resourcemanager.WorkflowUsageResourcePool) + if capabilityDonID != 0 { + identity = identity.WithDonID(strconv.FormatUint(uint64(capabilityDonID), 10)) + } + c.EVM.WithUsageMeter(c.usageMeter, identity) + if rmCfg.Emitter == nil { + c.lggr.Warnw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") + } + } // TODO: add org resolver capabilityID := fmt.Sprintf("%s (%d)", c.id, cfg.ChainID) @@ -199,6 +228,9 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor } startServices := []interface{ Start(context.Context) error }{c.consensusHandler, c.requestPoller, c.oracle, c.heightProvider, c.triggerService} + if c.usageMeter != nil { + startServices = append(startServices, c.usageMeter) + } for _, service := range startServices { if err := service.Start(ctx); err != nil { return err @@ -263,7 +295,11 @@ func (c *capabilityGRPCService) Start(_ context.Context) error { 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()) + errs := errors.Join(c.EVM.Close(), c.requestPoller.Close(), c.consensusHandler.Close(), c.oracle.Close(context.Background()), c.triggerService.Close(), c.heightProvider.Close()) + if c.usageMeter != nil { + errs = errors.Join(errs, c.usageMeter.Close()) + } + return errs } func (c *capabilityGRPCService) HealthReport() map[string]error { diff --git a/chain_capabilities/solana/actions/actions.go b/chain_capabilities/solana/actions/actions.go index e92fa84b4..4d97b11a9 100644 --- a/chain_capabilities/solana/actions/actions.go +++ b/chain_capabilities/solana/actions/actions.go @@ -25,6 +25,7 @@ import ( capmon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/monitoring" commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" @@ -55,6 +56,16 @@ type Solana struct { transmissionScheduler ts.TransmissionScheduler handler chainconsensus.RequestHandler forwarderState solgo.PublicKey + usageMeter *resourcemanager.ResourceManager + usageIdentity resourcemanager.ResourceIdentity +} + +// WithUsageMeter enables cre:workflow:gas usage MeterRecords for write reports. +// identity is the base metering identity (DON id already stamped). Nil rm +// disables emission. +func (s *Solana) WithUsageMeter(rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity) { + s.usageMeter = rm + s.usageIdentity = identity } func NewSolana(ctx context.Context, cfg *config.Config, s types.SolanaService, messageBuilder *monitoring.MessageBuilder, diff --git a/chain_capabilities/solana/actions/gas_usage_test.go b/chain_capabilities/solana/actions/gas_usage_test.go new file mode 100644 index 000000000..c0adad5fb --- /dev/null +++ b/chain_capabilities/solana/actions/gas_usage_test.go @@ -0,0 +1,80 @@ +package actions + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" + meteringpb "github.com/smartcontractkit/chainlink-protos/metering/go" +) + +type recordingEmitter struct{ records []*meteringpb.MeterRecord } + +func (r *recordingEmitter) Emit(_ context.Context, body []byte, _ ...any) error { + var rec meteringpb.MeterRecord + if err := proto.Unmarshal(body, &rec); err != nil { + return err + } + r.records = append(r.records, &rec) + return nil +} + +func TestEmitGasUsage(t *testing.T) { + metadata := capabilities.RequestMetadata{WorkflowID: "wf-1", WorkflowExecutionID: "exec-1", OrgID: "org-1"} + identity := resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite, ResourcePool: resourcemanager.WorkflowUsageResourcePool} + fee, _ := new(big.Int).SetString("123456789012345678901234", 10) + + t.Run("emits one gas record keyed by tx hash and logs the contract line", func(t *testing.T) { + lggr, obs := logger.TestObserved(t, zapcore.InfoLevel) + emitter := &recordingEmitter{} + rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) + + emitGasUsage(t.Context(), lggr, rm, identity, 421614, metadata, "0xabc", fee) + + require.Len(t, emitter.records, 1) + rec := emitter.records[0] + require.Equal(t, meteringpb.MeterAction_METER_ACTION_USAGE, rec.GetAction()) + require.Equal(t, resourcemanager.EmittingServiceChainWrite, rec.GetIdentity().GetService()) + require.Len(t, rec.GetUtilizations(), 1) + u := rec.GetUtilizations()[0] + require.Equal(t, "cre:workflow:gas:421614", u.GetResourceType()) + require.Equal(t, "wf-1:exec-1", u.GetResourceId()) + require.Equal(t, "0xabc", u.GetEventId()) + require.Equal(t, "org-1", u.GetOrgId()) + require.Equal(t, "123456789012345678901234", u.GetValue()) + + logs := obs.FilterMessage("Emitted capability usage meter record").All() + require.Len(t, logs, 1) + fields := logs[0].ContextMap() + require.Equal(t, "exec-1", fields["executionID"]) + require.Equal(t, "0xabc", fields["eventID"]) + require.Equal(t, "cre:workflow:gas:421614", fields["resourceType"]) + require.Equal(t, "123456789012345678901234", fields["value"]) + require.Equal(t, "org-1", fields["orgID"]) + }) + + t.Run("no-op without a meter or fee", func(t *testing.T) { + lggr := logger.Test(t) + emitter := &recordingEmitter{} + rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) + emitGasUsage(t.Context(), lggr, nil, identity, 1, metadata, "0xabc", fee) + emitGasUsage(t.Context(), lggr, rm, identity, 1, metadata, "0xabc", nil) + require.Empty(t, emitter.records) + }) + + t.Run("refuses a malformed resource id instead of emitting a bad record", func(t *testing.T) { + lggr, obs := logger.TestObserved(t, zapcore.ErrorLevel) + emitter := &recordingEmitter{} + rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) + emitGasUsage(t.Context(), lggr, rm, identity, 1, capabilities.RequestMetadata{WorkflowID: "", WorkflowExecutionID: "exec-1"}, "0xabc", fee) + require.Empty(t, emitter.records) + require.Len(t, obs.FilterMessage("Gas usage meter record not emitted").All(), 1) + }) +} diff --git a/chain_capabilities/solana/actions/write_report.go b/chain_capabilities/solana/actions/write_report.go index be79c0e23..08d2e5c49 100644 --- a/chain_capabilities/solana/actions/write_report.go +++ b/chain_capabilities/solana/actions/write_report.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "math/big" "strings" "time" @@ -21,6 +22,7 @@ import ( commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/contexts" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" "github.com/smartcontractkit/chainlink-common/pkg/types" soltypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/solana" @@ -68,6 +70,9 @@ type WriteReport struct { txComputeLimit limits.BoundLimiter[uint32] reportSizeLimit limits.BoundLimiter[commoncfg.Size] transmissionScheduler ts.TransmissionScheduler + + usageMeter *resourcemanager.ResourceManager + usageIdentity resourcemanager.ResourceIdentity } func (s *Solana) WriteReport( @@ -122,6 +127,8 @@ func (s *Solana) executeWriteReport(ctx context.Context, request *solcap.WriteRe beholderProcessor: s.beholderProcessor, messageBuilder: s.messageBuilder, transmissionScheduler: s.transmissionScheduler, + usageMeter: s.usageMeter, + usageIdentity: s.usageIdentity, } return wr.executeWriteReport(ctx, request, telemetryContext, metadata) @@ -618,9 +625,41 @@ func (wr *WriteReport) meteringFromTxSignature(ctx context.Context, telemetryCon monitoring.LogAndEmitError(ctx, wr.lggr, wr.beholderProcessor, wr.messageBuilder.BuildWriteReportTxFeeCalculationError(telemetryContext, request, sig, err.Error())) return capabilities.ResponseMetadata{} } + emitGasUsage(ctx, wr.lggr, wr.usageMeter, wr.usageIdentity, wr.chainSelector, telemetryContext.RequestMetadata, sig.String(), new(big.Int).SetUint64(feeInLamports)) return metering.GetResponseMetadataWriteReport(feeInLamports, wr.chainSelector) } +// emitGasUsage emits the cre:workflow:gas: usage MeterRecord for +// one chain write and logs the emission. The log line is a contract consumed by +// the billing reconciler (fields: executionID, eventID, resourceType, value, +// orgID, txHash) and must stay stable. Fail-open: never affects the reply. +func emitGasUsage(ctx context.Context, lggr logger.Logger, rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity, chainSelector uint64, metadata capabilities.RequestMetadata, txHash string, fee *big.Int) { + if rm == nil || fee == nil { + return + } + resourceID, err := resourcemanager.WorkflowUsageResourceID(metadata.WorkflowID, metadata.WorkflowExecutionID) + if err != nil { + lggr.Errorw("Gas usage meter record not emitted", "err", err, "executionID", metadata.WorkflowExecutionID) + return + } + resourceType := resourcemanager.WorkflowGasResourceType(chainSelector) + // The capability event id for gas is the transaction hash: one record per + // on-chain write, identical on every node of the DON that observes it. + rm.EmitUsageValue(ctx, identity, txHash, fee, resourcemanager.UtilizationFields{ + ResourceType: resourceType, + ResourceID: resourceID, + OrgID: metadata.OrgID, + }) + lggr.Infow("Emitted capability usage meter record", + "executionID", metadata.WorkflowExecutionID, + "eventID", txHash, + "resourceType", resourceType, + "value", fee.String(), + "orgID", metadata.OrgID, + "txHash", txHash, + ) +} + func (wr *WriteReport) getFee(ctx context.Context, sig solana.Signature) (uint64, error) { tx, err := wr.GetTransaction(ctx, soltypes.GetTransactionRequest{Signature: soltypes.Signature(sig)}) if err != nil { diff --git a/chain_capabilities/solana/go.mod b/chain_capabilities/solana/go.mod index e875dfcae..e49463a33 100644 --- a/chain_capabilities/solana/go.mod +++ b/chain_capabilities/solana/go.mod @@ -9,14 +9,16 @@ require ( github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/chain-selectors v1.0.104 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260917115705-1d3a14a9b049 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260821155228-fa1d775e2138 + github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260917175559-f7d4a86019d4 github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260803170030-90043607d911 github.com/smartcontractkit/libocr v0.0.0-20260810200708-618b5bf7f342 github.com/stretchr/testify v1.12.1 go.opentelemetry.io/otel v1.46.0 + go.uber.org/zap v1.28.0 google.golang.org/protobuf v1.36.12 ) @@ -134,7 +136,6 @@ require ( github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 // indirect github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect @@ -178,7 +179,6 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect - go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.55.0 // indirect diff --git a/chain_capabilities/solana/go.sum b/chain_capabilities/solana/go.sum index 144cd5360..060d71e04 100644 --- a/chain_capabilities/solana/go.sum +++ b/chain_capabilities/solana/go.sum @@ -634,8 +634,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288/go.mod h1:jPHlo/IN2YAArI001JJixmm6ZHQwgnAVJXY8VBFiFTc= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 h1:eEjTgIQn4RW0ZPRepUDYTdgGwaRCMawMwgXkHItUc9U= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260917115705-1d3a14a9b049 h1:WebE9HTubitMC5xq+xCAArEI+oBLekVOoxYnCkB8whI= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260917115705-1d3a14a9b049/go.mod h1:3CaCxSyZmgWl1PhxneFSZqOfN7DF2VnHML0bTfiDNIo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 h1:VY3d0Au+a5PzJUHjuJ7vc00DrRL6Wd/QlbfSLZqzhwE= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340 h1:PsjEI+5jZIz9AS4eOsLS5VpSWJINf38clXV3wryPyMk= diff --git a/chain_capabilities/solana/main.go b/chain_capabilities/solana/main.go index a95a11e66..132914fa5 100644 --- a/chain_capabilities/solana/main.go +++ b/chain_capabilities/solana/main.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" + "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -55,6 +56,13 @@ type capabilityGRPCService struct { lggr logger.Logger limitsFactory limits.Factory triggerService *trigger.SolanaLogTriggerService + + // meteringCfg and capabilityUsageEnabled come from the LOOP environment + // ([Metering] on the host). usageMeter emits cre:workflow:gas usage records + // for write reports when enabled; nil otherwise. + meteringCfg resourcemanager.Config + capabilityUsageEnabled bool + usageMeter *resourcemanager.ResourceManager } type capability struct { @@ -69,7 +77,12 @@ var _ solcapserver.ClientCapability = &capabilityGRPCService{} func main() { loopserver.ServeNew(CapabilityName, func(s *loop.Server) loop.StandardCapabilities { - return solcapserver.NewClientServer(&capabilityGRPCService{lggr: s.Logger.Named(CapabilityName), limitsFactory: s.LimitsFactory}) + return solcapserver.NewClientServer(&capabilityGRPCService{ + lggr: s.Logger.Named(CapabilityName), + limitsFactory: s.LimitsFactory, + meteringCfg: s.MeteringConfig(), + capabilityUsageEnabled: s.EnvConfig.CapabilityUsageEnabled, + }) }, loop.WithOtelViews(append(consMetrics.MetricViews(), capmon.MetricViews()...))) } @@ -109,6 +122,9 @@ func (c *capabilityGRPCService) Close() error { if c.Solana != nil { closers = append(closers, c.Solana) } + if c.usageMeter != nil { + closers = append(closers, c.usageMeter) + } return services.CloseAll(closers...) } @@ -240,6 +256,23 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor if err != nil { return err } + if c.capabilityUsageEnabled { + // Gated by [Metering].CapabilityUsageEnabled on the host, independent of + // MeterRecordsEnabled (durable resource metering), so force records on. + rmCfg := c.meteringCfg.ResourceManagerConfig + rmCfg.MeterRecordsEnabled = true + rmCfg.MeterSnapshotsEnabled = false + c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) + identity := resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, resourcemanager.WorkflowUsageResourcePool) + if dependencies.CapabilityDonID != 0 { + identity = identity.WithDonID(strconv.FormatUint(uint64(dependencies.CapabilityDonID), 10)) + } + c.Solana.WithUsageMeter(c.usageMeter, identity) + if rmCfg.Emitter == nil { + c.lggr.Warnw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") + } + toStart = append(toStart, c.usageMeter) + } c.triggerService, err = trigger.NewLogTriggerService(trigger.LogTriggerServiceOpts{ SolanaService: solService, From 54d764b7901543a7a53d4315c51585e85ae9bcf3 Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Mon, 5 Oct 2026 16:54:37 -0400 Subject: [PATCH 2/7] chain-write: gas usage identity pools per the billing payload contract Identity.ResourcePool "cre:workflow:gas" and ResourcePoolID "cre:workflow:gas:", derived via resourcemanager.WithWorkflowUsagePool from the plugin's chain selector, matching the Billing <-> Decentralized stack contract. Bumps chainlink-common. Co-Authored-By: Claude Fable 5.1 --- chain_capabilities/evm/actions/gas_usage_test.go | 4 +++- chain_capabilities/evm/go.mod | 2 +- chain_capabilities/evm/go.sum | 4 ++-- chain_capabilities/evm/main.go | 2 +- chain_capabilities/solana/actions/gas_usage_test.go | 4 +++- chain_capabilities/solana/go.mod | 2 +- chain_capabilities/solana/go.sum | 4 ++-- chain_capabilities/solana/main.go | 2 +- 8 files changed, 14 insertions(+), 10 deletions(-) diff --git a/chain_capabilities/evm/actions/gas_usage_test.go b/chain_capabilities/evm/actions/gas_usage_test.go index c0adad5fb..1a10c2868 100644 --- a/chain_capabilities/evm/actions/gas_usage_test.go +++ b/chain_capabilities/evm/actions/gas_usage_test.go @@ -28,7 +28,7 @@ func (r *recordingEmitter) Emit(_ context.Context, body []byte, _ ...any) error func TestEmitGasUsage(t *testing.T) { metadata := capabilities.RequestMetadata{WorkflowID: "wf-1", WorkflowExecutionID: "exec-1", OrgID: "org-1"} - identity := resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite, ResourcePool: resourcemanager.WorkflowUsageResourcePool} + identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite}, resourcemanager.WorkflowGasResourceType(421614)) fee, _ := new(big.Int).SetString("123456789012345678901234", 10) t.Run("emits one gas record keyed by tx hash and logs the contract line", func(t *testing.T) { @@ -42,6 +42,8 @@ func TestEmitGasUsage(t *testing.T) { rec := emitter.records[0] require.Equal(t, meteringpb.MeterAction_METER_ACTION_USAGE, rec.GetAction()) require.Equal(t, resourcemanager.EmittingServiceChainWrite, rec.GetIdentity().GetService()) + require.Equal(t, "cre:workflow:gas", rec.GetIdentity().GetResourcePool()) + require.Equal(t, "cre:workflow:gas:421614", rec.GetIdentity().GetResourcePoolId()) require.Len(t, rec.GetUtilizations(), 1) u := rec.GetUtilizations()[0] require.Equal(t, "cre:workflow:gas:421614", u.GetResourceType()) diff --git a/chain_capabilities/evm/go.mod b/chain_capabilities/evm/go.mod index e9d4d0667..96177230e 100644 --- a/chain_capabilities/evm/go.mod +++ b/chain_capabilities/evm/go.mod @@ -8,7 +8,7 @@ require ( github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/chain-selectors v1.0.111 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 diff --git a/chain_capabilities/evm/go.sum b/chain_capabilities/evm/go.sum index 8ed661092..293eafd7f 100644 --- a/chain_capabilities/evm/go.sum +++ b/chain_capabilities/evm/go.sum @@ -458,8 +458,8 @@ github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866/go.mod h1:7q+YF5LlwVYqVPMLBgZ5gLQyCbgaOt3NsvD0gFfOhSM= github.com/smartcontractkit/chain-selectors v1.0.111 h1:urb/qOyRanxDn+Wras+LULHEHSlJ4tYnfeAHzeMBn5U= github.com/smartcontractkit/chain-selectors v1.0.111/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 h1:VY3d0Au+a5PzJUHjuJ7vc00DrRL6Wd/QlbfSLZqzhwE= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 h1:g/r+BkcwvHlQ017TZPhb/VPk05xcR32EHUAO5ANi5Yo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049 h1:nQ2bw8QQLT+AuCE6JUVIRT1RAo08wQHR9jWh+MgT2GA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049/go.mod h1:FO9igruzwHPXstyb9J9z83sJ19iLSNSIg+0H/n0wlJU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= diff --git a/chain_capabilities/evm/main.go b/chain_capabilities/evm/main.go index 16c8524ff..d41b1c9fc 100644 --- a/chain_capabilities/evm/main.go +++ b/chain_capabilities/evm/main.go @@ -189,7 +189,7 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor rmCfg.MeterRecordsEnabled = true rmCfg.MeterSnapshotsEnabled = false c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) - identity := resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, resourcemanager.WorkflowUsageResourcePool) + identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, ""), resourcemanager.WorkflowGasResourceType(c.chainSelector)) if capabilityDonID != 0 { identity = identity.WithDonID(strconv.FormatUint(uint64(capabilityDonID), 10)) } diff --git a/chain_capabilities/solana/actions/gas_usage_test.go b/chain_capabilities/solana/actions/gas_usage_test.go index c0adad5fb..1a10c2868 100644 --- a/chain_capabilities/solana/actions/gas_usage_test.go +++ b/chain_capabilities/solana/actions/gas_usage_test.go @@ -28,7 +28,7 @@ func (r *recordingEmitter) Emit(_ context.Context, body []byte, _ ...any) error func TestEmitGasUsage(t *testing.T) { metadata := capabilities.RequestMetadata{WorkflowID: "wf-1", WorkflowExecutionID: "exec-1", OrgID: "org-1"} - identity := resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite, ResourcePool: resourcemanager.WorkflowUsageResourcePool} + identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite}, resourcemanager.WorkflowGasResourceType(421614)) fee, _ := new(big.Int).SetString("123456789012345678901234", 10) t.Run("emits one gas record keyed by tx hash and logs the contract line", func(t *testing.T) { @@ -42,6 +42,8 @@ func TestEmitGasUsage(t *testing.T) { rec := emitter.records[0] require.Equal(t, meteringpb.MeterAction_METER_ACTION_USAGE, rec.GetAction()) require.Equal(t, resourcemanager.EmittingServiceChainWrite, rec.GetIdentity().GetService()) + require.Equal(t, "cre:workflow:gas", rec.GetIdentity().GetResourcePool()) + require.Equal(t, "cre:workflow:gas:421614", rec.GetIdentity().GetResourcePoolId()) require.Len(t, rec.GetUtilizations(), 1) u := rec.GetUtilizations()[0] require.Equal(t, "cre:workflow:gas:421614", u.GetResourceType()) diff --git a/chain_capabilities/solana/go.mod b/chain_capabilities/solana/go.mod index e49463a33..db4e62c4d 100644 --- a/chain_capabilities/solana/go.mod +++ b/chain_capabilities/solana/go.mod @@ -9,7 +9,7 @@ require ( github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/chain-selectors v1.0.104 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260821155228-fa1d775e2138 github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe diff --git a/chain_capabilities/solana/go.sum b/chain_capabilities/solana/go.sum index 060d71e04..ebc2736dc 100644 --- a/chain_capabilities/solana/go.sum +++ b/chain_capabilities/solana/go.sum @@ -634,8 +634,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288/go.mod h1:jPHlo/IN2YAArI001JJixmm6ZHQwgnAVJXY8VBFiFTc= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 h1:eEjTgIQn4RW0ZPRepUDYTdgGwaRCMawMwgXkHItUc9U= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4 h1:VY3d0Au+a5PzJUHjuJ7vc00DrRL6Wd/QlbfSLZqzhwE= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260929173517-9b19176347e4/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 h1:g/r+BkcwvHlQ017TZPhb/VPk05xcR32EHUAO5ANi5Yo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340 h1:PsjEI+5jZIz9AS4eOsLS5VpSWJINf38clXV3wryPyMk= diff --git a/chain_capabilities/solana/main.go b/chain_capabilities/solana/main.go index 132914fa5..450038e40 100644 --- a/chain_capabilities/solana/main.go +++ b/chain_capabilities/solana/main.go @@ -263,7 +263,7 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor rmCfg.MeterRecordsEnabled = true rmCfg.MeterSnapshotsEnabled = false c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) - identity := resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, resourcemanager.WorkflowUsageResourcePool) + identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, ""), resourcemanager.WorkflowGasResourceType(c.chainSelector)) if dependencies.CapabilityDonID != 0 { identity = identity.WithDonID(strconv.FormatUint(uint64(dependencies.CapabilityDonID), 10)) } From 8bdfbf131155e7160eec1992a1033124cd38fa09 Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Wed, 7 Oct 2026 11:44:19 -0400 Subject: [PATCH 3/7] chain-write: gate gas usage records on MeterRecordsEnabled, drop CapabilityUsageEnabled chainlink-common #2428 dropped the separate CapabilityUsageEnabled flag per review: usage records share the host's [Metering].MeterRecordsEnabled gate with durable resource metering. The EVM and Solana plugins now build the gas usage ResourceManager from MeteringConfig when MeterRecordsEnabled is set, snapshots off. Bumps chainlink-common to main (29594528, includes #2428) in both modules. Co-Authored-By: Claude Fable 5.1 --- chain_capabilities/evm/go.mod | 4 ++-- chain_capabilities/evm/go.sum | 6 ++++++ chain_capabilities/evm/main.go | 23 ++++++++++------------- chain_capabilities/solana/go.mod | 4 ++-- chain_capabilities/solana/go.sum | 6 ++++++ chain_capabilities/solana/main.go | 25 +++++++++++-------------- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/chain_capabilities/evm/go.mod b/chain_capabilities/evm/go.mod index 96177230e..891bf72b9 100644 --- a/chain_capabilities/evm/go.mod +++ b/chain_capabilities/evm/go.mod @@ -8,7 +8,7 @@ require ( github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/chain-selectors v1.0.111 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006173051-4267c912d752 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 @@ -142,7 +142,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 // 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/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect diff --git a/chain_capabilities/evm/go.sum b/chain_capabilities/evm/go.sum index 293eafd7f..0d6c7f652 100644 --- a/chain_capabilities/evm/go.sum +++ b/chain_capabilities/evm/go.sum @@ -460,10 +460,16 @@ github.com/smartcontractkit/chain-selectors v1.0.111 h1:urb/qOyRanxDn+Wras+LULHE github.com/smartcontractkit/chain-selectors v1.0.111/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 h1:g/r+BkcwvHlQ017TZPhb/VPk05xcR32EHUAO5ANi5Yo= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006171042-48f88a916c03 h1:J8BaKmRgrEJmF0WoSCLf/RFvu+yVpFkjER9QVqVZBTQ= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006171042-48f88a916c03/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006173051-4267c912d752 h1:4XaOIy4h5L7OT960w/dTn1Bht5xfYnZEpHtqfEO9Xns= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006173051-4267c912d752/go.mod h1:37Mu0pbAnGmHPgOZ8bQvgP4hzXw/Wu7smKX4cN46UOs= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049 h1:nQ2bw8QQLT+AuCE6JUVIRT1RAo08wQHR9jWh+MgT2GA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049/go.mod h1:FO9igruzwHPXstyb9J9z83sJ19iLSNSIg+0H/n0wlJU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 h1:J4vYRJxkBO8Yf//17wScKjYWmS45VZaFWx0zt+4ETEU= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 h1:n+V23NCROMeWFHUyR78Z4dXkUnPV3K8RzOzJpnXuXV0= github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744/go.mod h1:dEk48o9w++AyLCv12fNKIJf4Ko6iywWWJAGrjfS0vR8= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 h1:JFo7C3FilwhfwGBLAyj2umbL+P4QxGmVi/b8yt9kqvI= diff --git a/chain_capabilities/evm/main.go b/chain_capabilities/evm/main.go index d41b1c9fc..f9cc7c23d 100644 --- a/chain_capabilities/evm/main.go +++ b/chain_capabilities/evm/main.go @@ -59,11 +59,10 @@ type capabilityGRPCService struct { lggr logger.Logger limitsFactory limits.Factory - // meteringCfg and capabilityUsageEnabled come from the LOOP environment - // ([Metering] on the host). usageMeter emits cre:workflow:gas usage records - // for write reports when enabled; nil otherwise. - meteringCfg resourcemanager.Config - capabilityUsageEnabled bool + // meteringCfg comes from the LOOP environment ([Metering] on the host). + // usageMeter emits cre:workflow:gas usage records for write reports when + // MeterRecordsEnabled; nil otherwise. + meteringCfg resourcemanager.Config } type capability struct { @@ -82,10 +81,9 @@ var _ evmcapserver.ClientCapability = &capabilityGRPCService{} func main() { loopserver.ServeNew(CapabilityName, func(s *loop.Server) loop.StandardCapabilities { return evmcapserver.NewClientServer(&capabilityGRPCService{ - lggr: s.Logger, - limitsFactory: s.LimitsFactory, - meteringCfg: s.MeteringConfig(), - capabilityUsageEnabled: s.EnvConfig.CapabilityUsageEnabled, + lggr: s.Logger, + limitsFactory: s.LimitsFactory, + meteringCfg: s.MeteringConfig(), }) }, loop.WithOtelViews(append(consMetrics.MetricViews(), monitoring.MetricViews()...))) } @@ -182,11 +180,10 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor if err != nil { return fmt.Errorf("failed to init evm relayer for chainID %d from relayer: %w", cfg.ChainID, err) } - if c.capabilityUsageEnabled { - // Gated by [Metering].CapabilityUsageEnabled on the host, independent of - // MeterRecordsEnabled (durable resource metering), so force records on. + if c.meteringCfg.MeterRecordsEnabled { + // Gas usage records share the host's [Metering].MeterRecordsEnabled gate + // with durable resource metering; they are never snapshotted. rmCfg := c.meteringCfg.ResourceManagerConfig - rmCfg.MeterRecordsEnabled = true rmCfg.MeterSnapshotsEnabled = false c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, ""), resourcemanager.WorkflowGasResourceType(c.chainSelector)) diff --git a/chain_capabilities/solana/go.mod b/chain_capabilities/solana/go.mod index db4e62c4d..8725b2d2e 100644 --- a/chain_capabilities/solana/go.mod +++ b/chain_capabilities/solana/go.mod @@ -9,7 +9,7 @@ require ( github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/capabilities/libs v0.0.0-20260922114021-5cee95428866 github.com/smartcontractkit/chain-selectors v1.0.104 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006173051-4267c912d752 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260821155228-fa1d775e2138 github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe @@ -133,7 +133,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 // indirect github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243 // 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-20260709145319-7782fb89eb16 // indirect diff --git a/chain_capabilities/solana/go.sum b/chain_capabilities/solana/go.sum index ebc2736dc..3ed533e5f 100644 --- a/chain_capabilities/solana/go.sum +++ b/chain_capabilities/solana/go.sum @@ -636,8 +636,14 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9 h1:g/r+BkcwvHlQ017TZPhb/VPk05xcR32EHUAO5ANi5Yo= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261005205238-96be6dc8c2b9/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006171042-48f88a916c03 h1:J8BaKmRgrEJmF0WoSCLf/RFvu+yVpFkjER9QVqVZBTQ= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006171042-48f88a916c03/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006173051-4267c912d752 h1:4XaOIy4h5L7OT960w/dTn1Bht5xfYnZEpHtqfEO9Xns= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006173051-4267c912d752/go.mod h1:37Mu0pbAnGmHPgOZ8bQvgP4hzXw/Wu7smKX4cN46UOs= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 h1:J4vYRJxkBO8Yf//17wScKjYWmS45VZaFWx0zt+4ETEU= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340 h1:PsjEI+5jZIz9AS4eOsLS5VpSWJINf38clXV3wryPyMk= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340/go.mod h1:P/0OSXUlFaxxD4B/P6HWbxYtIRmmWGDJAvanq19879c= github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243 h1:vaFBupfFfImQgqOeuC7Muk2GflbYP6Gpi0Y/TLroFU8= diff --git a/chain_capabilities/solana/main.go b/chain_capabilities/solana/main.go index 450038e40..b32a88ff2 100644 --- a/chain_capabilities/solana/main.go +++ b/chain_capabilities/solana/main.go @@ -57,12 +57,11 @@ type capabilityGRPCService struct { limitsFactory limits.Factory triggerService *trigger.SolanaLogTriggerService - // meteringCfg and capabilityUsageEnabled come from the LOOP environment - // ([Metering] on the host). usageMeter emits cre:workflow:gas usage records - // for write reports when enabled; nil otherwise. - meteringCfg resourcemanager.Config - capabilityUsageEnabled bool - usageMeter *resourcemanager.ResourceManager + // meteringCfg comes from the LOOP environment ([Metering] on the host). + // usageMeter emits cre:workflow:gas usage records for write reports when + // MeterRecordsEnabled; nil otherwise. + meteringCfg resourcemanager.Config + usageMeter *resourcemanager.ResourceManager } type capability struct { @@ -78,10 +77,9 @@ var _ solcapserver.ClientCapability = &capabilityGRPCService{} func main() { loopserver.ServeNew(CapabilityName, func(s *loop.Server) loop.StandardCapabilities { return solcapserver.NewClientServer(&capabilityGRPCService{ - lggr: s.Logger.Named(CapabilityName), - limitsFactory: s.LimitsFactory, - meteringCfg: s.MeteringConfig(), - capabilityUsageEnabled: s.EnvConfig.CapabilityUsageEnabled, + lggr: s.Logger.Named(CapabilityName), + limitsFactory: s.LimitsFactory, + meteringCfg: s.MeteringConfig(), }) }, loop.WithOtelViews(append(consMetrics.MetricViews(), capmon.MetricViews()...))) } @@ -256,11 +254,10 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor if err != nil { return err } - if c.capabilityUsageEnabled { - // Gated by [Metering].CapabilityUsageEnabled on the host, independent of - // MeterRecordsEnabled (durable resource metering), so force records on. + if c.meteringCfg.MeterRecordsEnabled { + // Gas usage records share the host's [Metering].MeterRecordsEnabled gate + // with durable resource metering; they are never snapshotted. rmCfg := c.meteringCfg.ResourceManagerConfig - rmCfg.MeterRecordsEnabled = true rmCfg.MeterSnapshotsEnabled = false c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, ""), resourcemanager.WorkflowGasResourceType(c.chainSelector)) From 8c96721c8c252be8a61cf7219ff145f09e1c9cdb Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Wed, 7 Oct 2026 11:53:02 -0400 Subject: [PATCH 4/7] chain-write: drop gate comment, log missing durable emitter at error level --- chain_capabilities/evm/main.go | 4 +--- chain_capabilities/solana/main.go | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/chain_capabilities/evm/main.go b/chain_capabilities/evm/main.go index d746f846c..44b4dcd5e 100644 --- a/chain_capabilities/evm/main.go +++ b/chain_capabilities/evm/main.go @@ -181,8 +181,6 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor return fmt.Errorf("failed to init evm relayer for chainID %d from relayer: %w", cfg.ChainID, err) } if c.meteringCfg.MeterRecordsEnabled { - // Gas usage records share the host's [Metering].MeterRecordsEnabled gate - // with durable resource metering; they are never snapshotted. rmCfg := c.meteringCfg.ResourceManagerConfig rmCfg.MeterSnapshotsEnabled = false c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) @@ -192,7 +190,7 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor } c.EVM.WithUsageMeter(c.usageMeter, identity) if rmCfg.Emitter == nil { - c.lggr.Warnw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") + c.lggr.Errorw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") } } diff --git a/chain_capabilities/solana/main.go b/chain_capabilities/solana/main.go index 6212f484e..802798c96 100644 --- a/chain_capabilities/solana/main.go +++ b/chain_capabilities/solana/main.go @@ -255,8 +255,6 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor return err } if c.meteringCfg.MeterRecordsEnabled { - // Gas usage records share the host's [Metering].MeterRecordsEnabled gate - // with durable resource metering; they are never snapshotted. rmCfg := c.meteringCfg.ResourceManagerConfig rmCfg.MeterSnapshotsEnabled = false c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) @@ -266,7 +264,7 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor } c.Solana.WithUsageMeter(c.usageMeter, identity) if rmCfg.Emitter == nil { - c.lggr.Warnw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") + c.lggr.Errorw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") } toStart = append(toStart, c.usageMeter) } From c9fc01e8e927d63bf844d0a25fd4e8c897ad8078 Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Wed, 7 Oct 2026 12:05:08 -0400 Subject: [PATCH 5/7] chain-write: use shared gasmeter for gas usage records Replaces the duplicated ResourceManager wiring, emit function and tests in the EVM and Solana plugins with chain_capabilities/common/gasmeter (#813). Each plugin now builds one gasmeter.Meter from the LOOP metering config and calls Emit at its fee-resolved point. Pins the common module to the #813 branch until it merges. Co-Authored-By: Claude Fable 5.1 --- chain_capabilities/evm/actions/actions.go | 15 ++-- .../evm/actions/gas_usage_test.go | 82 ------------------- .../evm/actions/write_report.go | 41 +--------- chain_capabilities/evm/go.mod | 2 +- chain_capabilities/evm/go.sum | 2 + chain_capabilities/evm/main.go | 29 +++---- chain_capabilities/solana/actions/actions.go | 15 ++-- .../solana/actions/gas_usage_test.go | 82 ------------------- .../solana/actions/write_report.go | 41 +--------- chain_capabilities/solana/go.mod | 2 +- chain_capabilities/solana/go.sum | 2 + chain_capabilities/solana/main.go | 26 ++---- 12 files changed, 45 insertions(+), 294 deletions(-) delete mode 100644 chain_capabilities/evm/actions/gas_usage_test.go delete mode 100644 chain_capabilities/solana/actions/gas_usage_test.go diff --git a/chain_capabilities/evm/actions/actions.go b/chain_capabilities/evm/actions/actions.go index 71df33fc2..2a1903f9e 100644 --- a/chain_capabilities/evm/actions/actions.go +++ b/chain_capabilities/evm/actions/actions.go @@ -21,7 +21,6 @@ import ( evmservice "github.com/smartcontractkit/chainlink-common/pkg/chains/evm" commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" @@ -32,6 +31,7 @@ import ( valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" + "github.com/smartcontractkit/capabilities/chain_capabilities/common/gasmeter" ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" "github.com/smartcontractkit/capabilities/chain_capabilities/evm/config" "github.com/smartcontractkit/capabilities/chain_capabilities/evm/internal/contracts" @@ -45,8 +45,7 @@ type EVM struct { types.EVMService consensusHandler chainconsensus.RequestHandler chainSelector uint64 - usageMeter *resourcemanager.ResourceManager - usageIdentity resourcemanager.ResourceIdentity + gasMeter *gasmeter.Meter keystoneForwarderAddress common.Address forwarderClient contracts.CREForwarderClient ReceiverGasMinimum uint64 @@ -98,12 +97,10 @@ func NewEVM(cfg config.Config, evmService types.EVMService, lggr logger.Logger, return e, nil } -// WithUsageMeter enables cre:workflow:gas usage MeterRecords for write reports. -// identity is the base metering identity (DON id already stamped). Nil rm -// disables emission. -func (e *EVM) WithUsageMeter(rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity) { - e.usageMeter = rm - e.usageIdentity = identity +// WithGasMeter enables cre:workflow:gas usage MeterRecords for write reports. +// A nil meter disables emission. +func (e *EVM) WithGasMeter(m *gasmeter.Meter) { + e.gasMeter = m } func (e *EVM) initLimiters(limitsFactory limits.Factory) (err error) { diff --git a/chain_capabilities/evm/actions/gas_usage_test.go b/chain_capabilities/evm/actions/gas_usage_test.go deleted file mode 100644 index 1a10c2868..000000000 --- a/chain_capabilities/evm/actions/gas_usage_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package actions - -import ( - "context" - "math/big" - "testing" - - "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" - meteringpb "github.com/smartcontractkit/chainlink-protos/metering/go" -) - -type recordingEmitter struct{ records []*meteringpb.MeterRecord } - -func (r *recordingEmitter) Emit(_ context.Context, body []byte, _ ...any) error { - var rec meteringpb.MeterRecord - if err := proto.Unmarshal(body, &rec); err != nil { - return err - } - r.records = append(r.records, &rec) - return nil -} - -func TestEmitGasUsage(t *testing.T) { - metadata := capabilities.RequestMetadata{WorkflowID: "wf-1", WorkflowExecutionID: "exec-1", OrgID: "org-1"} - identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite}, resourcemanager.WorkflowGasResourceType(421614)) - fee, _ := new(big.Int).SetString("123456789012345678901234", 10) - - t.Run("emits one gas record keyed by tx hash and logs the contract line", func(t *testing.T) { - lggr, obs := logger.TestObserved(t, zapcore.InfoLevel) - emitter := &recordingEmitter{} - rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) - - emitGasUsage(t.Context(), lggr, rm, identity, 421614, metadata, "0xabc", fee) - - require.Len(t, emitter.records, 1) - rec := emitter.records[0] - require.Equal(t, meteringpb.MeterAction_METER_ACTION_USAGE, rec.GetAction()) - require.Equal(t, resourcemanager.EmittingServiceChainWrite, rec.GetIdentity().GetService()) - require.Equal(t, "cre:workflow:gas", rec.GetIdentity().GetResourcePool()) - require.Equal(t, "cre:workflow:gas:421614", rec.GetIdentity().GetResourcePoolId()) - require.Len(t, rec.GetUtilizations(), 1) - u := rec.GetUtilizations()[0] - require.Equal(t, "cre:workflow:gas:421614", u.GetResourceType()) - require.Equal(t, "wf-1:exec-1", u.GetResourceId()) - require.Equal(t, "0xabc", u.GetEventId()) - require.Equal(t, "org-1", u.GetOrgId()) - require.Equal(t, "123456789012345678901234", u.GetValue()) - - logs := obs.FilterMessage("Emitted capability usage meter record").All() - require.Len(t, logs, 1) - fields := logs[0].ContextMap() - require.Equal(t, "exec-1", fields["executionID"]) - require.Equal(t, "0xabc", fields["eventID"]) - require.Equal(t, "cre:workflow:gas:421614", fields["resourceType"]) - require.Equal(t, "123456789012345678901234", fields["value"]) - require.Equal(t, "org-1", fields["orgID"]) - }) - - t.Run("no-op without a meter or fee", func(t *testing.T) { - lggr := logger.Test(t) - emitter := &recordingEmitter{} - rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) - emitGasUsage(t.Context(), lggr, nil, identity, 1, metadata, "0xabc", fee) - emitGasUsage(t.Context(), lggr, rm, identity, 1, metadata, "0xabc", nil) - require.Empty(t, emitter.records) - }) - - t.Run("refuses a malformed resource id instead of emitting a bad record", func(t *testing.T) { - lggr, obs := logger.TestObserved(t, zapcore.ErrorLevel) - emitter := &recordingEmitter{} - rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) - emitGasUsage(t.Context(), lggr, rm, identity, 1, capabilities.RequestMetadata{WorkflowID: "", WorkflowExecutionID: "exec-1"}, "0xabc", fee) - require.Empty(t, emitter.records) - require.Len(t, obs.FilterMessage("Gas usage meter record not emitted").All(), 1) - }) -} diff --git a/chain_capabilities/evm/actions/write_report.go b/chain_capabilities/evm/actions/write_report.go index 047e39c4d..418b4f9ae 100644 --- a/chain_capabilities/evm/actions/write_report.go +++ b/chain_capabilities/evm/actions/write_report.go @@ -21,11 +21,11 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" evmtypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/evm" "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" + "github.com/smartcontractkit/capabilities/chain_capabilities/common/gasmeter" ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" "github.com/smartcontractkit/capabilities/chain_capabilities/evm/internal/contracts" @@ -54,8 +54,7 @@ type WriteReport struct { transmissionScheduler ts.TransmissionScheduler executionTimestamp time.Time - usageMeter *resourcemanager.ResourceManager - usageIdentity resourcemanager.ResourceIdentity + gasMeter *gasmeter.Meter } func (e *EVM) WriteReport(ctx context.Context, metadata capabilities.RequestMetadata, input *evm.WriteReportRequest) (*capabilities.ResponseAndMetadata[*evm.WriteReportReply], caperrors.Error) { @@ -101,8 +100,7 @@ func (e *EVM) executeWriteReport(ctx context.Context, request *evm.WriteReportRe writeReportL1FeeActive: e.writeReportL1FeeActive, transmissionScheduler: e.transmissionScheduler, executionTimestamp: metadata.ExecutionTimestamp, - usageMeter: e.usageMeter, - usageIdentity: e.usageIdentity, + gasMeter: e.gasMeter, } return wr.executeWriteReport(ctx, request, metadata, telemetryContext) @@ -541,41 +539,10 @@ func (e *WriteReport) meteringFromReply(ctx context.Context, metadata capabiliti e.lggr.Warnw("Transaction fee unavailable in reply; skipping metering", "txHash", hex.EncodeToString(reply.TxHash)) return capabilities.ResponseMetadata{} } - emitGasUsage(ctx, e.lggr, e.usageMeter, e.usageIdentity, e.chainSelector, metadata, hex.EncodeToString(reply.TxHash), feeInWei) + e.gasMeter.Emit(ctx, metadata, hex.EncodeToString(reply.TxHash), feeInWei) return metering.GetResponseMetadataWriteReport(feeInWei, e.chainSelector) } -// emitGasUsage emits the cre:workflow:gas: usage MeterRecord for -// one chain write and logs the emission. The log line is a contract consumed by -// the billing reconciler (fields: executionID, eventID, resourceType, value, -// orgID, txHash) and must stay stable. Fail-open: never affects the reply. -func emitGasUsage(ctx context.Context, lggr logger.Logger, rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity, chainSelector uint64, metadata capabilities.RequestMetadata, txHash string, fee *big.Int) { - if rm == nil || fee == nil { - return - } - resourceID, err := resourcemanager.WorkflowUsageResourceID(metadata.WorkflowID, metadata.WorkflowExecutionID) - if err != nil { - lggr.Errorw("Gas usage meter record not emitted", "err", err, "executionID", metadata.WorkflowExecutionID) - return - } - resourceType := resourcemanager.WorkflowGasResourceType(chainSelector) - // The capability event id for gas is the transaction hash: one record per - // on-chain write, identical on every node of the DON that observes it. - rm.EmitUsageValue(ctx, identity, txHash, fee, resourcemanager.UtilizationFields{ - ResourceType: resourceType, - ResourceID: resourceID, - OrgID: metadata.OrgID, - }) - lggr.Infow("Emitted capability usage meter record", - "executionID", metadata.WorkflowExecutionID, - "eventID", txHash, - "resourceType", resourceType, - "value", fee.String(), - "orgID", metadata.OrgID, - "txHash", txHash, - ) -} - func (e *WriteReport) includeL1FeeInReceiptFee(ctx context.Context) bool { if e.writeReportL1FeeActive == nil { return false diff --git a/chain_capabilities/evm/go.mod b/chain_capabilities/evm/go.mod index d5101681c..40fb45ab8 100644 --- a/chain_capabilities/evm/go.mod +++ b/chain_capabilities/evm/go.mod @@ -5,7 +5,7 @@ go 1.27.1 require ( github.com/ethereum/go-ethereum v1.17.4 github.com/google/go-cmp v0.7.0 - github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 + github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 github.com/smartcontractkit/chain-selectors v1.0.112 github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464 diff --git a/chain_capabilities/evm/go.sum b/chain_capabilities/evm/go.sum index 42063d539..60ea408a5 100644 --- a/chain_capabilities/evm/go.sum +++ b/chain_capabilities/evm/go.sum @@ -457,6 +457,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 h1:nJ3V1j6QSFJ3zfwTO8Ty5TVNEb7g4HUkr9VXXV7hLSg= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866/go.mod h1:URIXl+ew48QapONMSLC1faCcGRnm5CRL3jj1PtpJt5E= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 h1:I3k5OaOI3BwdFH0GCIcibPnKD6HaxngU6z6bppPCx8g= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 h1:2+0wOWEiYcEm0XlS1HwgH09y/3qZ0zjokTdbExY2VQQ= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961/go.mod h1:h4kazMLsDM6CsKl2OnT9AtCHhhRsFptA/OTM7Hkj2GE= github.com/smartcontractkit/chain-selectors v1.0.112 h1:3VO+ttHsqGzPviMmb1AsIT5+rXVgz/Xx6wmvTpamrzE= diff --git a/chain_capabilities/evm/main.go b/chain_capabilities/evm/main.go index 44b4dcd5e..05eaa30ce 100644 --- a/chain_capabilities/evm/main.go +++ b/chain_capabilities/evm/main.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + chainselectors "github.com/smartcontractkit/chain-selectors" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" @@ -22,6 +23,7 @@ import ( "github.com/smartcontractkit/capabilities/libs/chainconsensus/oracle" "github.com/smartcontractkit/capabilities/libs/chainconsensus/poller" + "github.com/smartcontractkit/capabilities/chain_capabilities/common/gasmeter" 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" @@ -60,14 +62,14 @@ type capabilityGRPCService struct { limitsFactory limits.Factory // meteringCfg comes from the LOOP environment ([Metering] on the host). - // usageMeter emits cre:workflow:gas usage records for write reports when + // gasMeter emits cre:workflow:gas usage records for write reports when // MeterRecordsEnabled; nil otherwise. meteringCfg resourcemanager.Config } type capability struct { *actions.EVM - usageMeter *resourcemanager.ResourceManager + gasMeter *gasmeter.Meter id string requestPoller *poller.Poller consensusHandler chainconsensus.Handler @@ -180,19 +182,8 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor if err != nil { return fmt.Errorf("failed to init evm relayer for chainID %d from relayer: %w", cfg.ChainID, err) } - if c.meteringCfg.MeterRecordsEnabled { - rmCfg := c.meteringCfg.ResourceManagerConfig - rmCfg.MeterSnapshotsEnabled = false - c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) - identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, ""), resourcemanager.WorkflowGasResourceType(c.chainSelector)) - if capabilityDonID != 0 { - identity = identity.WithDonID(strconv.FormatUint(uint64(capabilityDonID), 10)) - } - c.EVM.WithUsageMeter(c.usageMeter, identity) - if rmCfg.Emitter == nil { - c.lggr.Errorw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") - } - } + c.gasMeter = gasmeter.New(c.lggr, c.meteringCfg, c.chainSelector, capabilityDonID) + c.EVM.WithGasMeter(c.gasMeter) // TODO: add org resolver capabilityID := fmt.Sprintf("%s (%d)", c.id, cfg.ChainID) @@ -223,8 +214,8 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor } startServices := []interface{ Start(context.Context) error }{c.consensusHandler, c.requestPoller, c.oracle, c.heightProvider, c.triggerService} - if c.usageMeter != nil { - startServices = append(startServices, c.usageMeter) + if c.gasMeter != nil { + startServices = append(startServices, c.gasMeter) } for _, service := range startServices { if err := service.Start(ctx); err != nil { @@ -291,8 +282,8 @@ func (c *capabilityGRPCService) Start(_ context.Context) error { func (c *capabilityGRPCService) Close() error { c.lggr.Infof("Closing %s", CapabilityName) errs := errors.Join(c.EVM.Close(), c.requestPoller.Close(), c.consensusHandler.Close(), c.oracle.Close(context.Background()), c.triggerService.Close(), c.heightProvider.Close()) - if c.usageMeter != nil { - errs = errors.Join(errs, c.usageMeter.Close()) + if c.gasMeter != nil { + errs = errors.Join(errs, c.gasMeter.Close()) } return errs } diff --git a/chain_capabilities/solana/actions/actions.go b/chain_capabilities/solana/actions/actions.go index 4d97b11a9..f42a503d6 100644 --- a/chain_capabilities/solana/actions/actions.go +++ b/chain_capabilities/solana/actions/actions.go @@ -25,7 +25,6 @@ import ( capmon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/monitoring" commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" @@ -35,6 +34,7 @@ import ( valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" + "github.com/smartcontractkit/capabilities/chain_capabilities/common/gasmeter" ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" "github.com/smartcontractkit/capabilities/chain_capabilities/solana/config" "github.com/smartcontractkit/capabilities/chain_capabilities/solana/monitoring" @@ -56,16 +56,13 @@ type Solana struct { transmissionScheduler ts.TransmissionScheduler handler chainconsensus.RequestHandler forwarderState solgo.PublicKey - usageMeter *resourcemanager.ResourceManager - usageIdentity resourcemanager.ResourceIdentity + gasMeter *gasmeter.Meter } -// WithUsageMeter enables cre:workflow:gas usage MeterRecords for write reports. -// identity is the base metering identity (DON id already stamped). Nil rm -// disables emission. -func (s *Solana) WithUsageMeter(rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity) { - s.usageMeter = rm - s.usageIdentity = identity +// WithGasMeter enables cre:workflow:gas usage MeterRecords for write reports. +// A nil meter disables emission. +func (s *Solana) WithGasMeter(m *gasmeter.Meter) { + s.gasMeter = m } func NewSolana(ctx context.Context, cfg *config.Config, s types.SolanaService, messageBuilder *monitoring.MessageBuilder, diff --git a/chain_capabilities/solana/actions/gas_usage_test.go b/chain_capabilities/solana/actions/gas_usage_test.go deleted file mode 100644 index 1a10c2868..000000000 --- a/chain_capabilities/solana/actions/gas_usage_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package actions - -import ( - "context" - "math/big" - "testing" - - "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" - meteringpb "github.com/smartcontractkit/chainlink-protos/metering/go" -) - -type recordingEmitter struct{ records []*meteringpb.MeterRecord } - -func (r *recordingEmitter) Emit(_ context.Context, body []byte, _ ...any) error { - var rec meteringpb.MeterRecord - if err := proto.Unmarshal(body, &rec); err != nil { - return err - } - r.records = append(r.records, &rec) - return nil -} - -func TestEmitGasUsage(t *testing.T) { - metadata := capabilities.RequestMetadata{WorkflowID: "wf-1", WorkflowExecutionID: "exec-1", OrgID: "org-1"} - identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.ResourceIdentity{Product: "cre", Service: resourcemanager.EmittingServiceChainWrite}, resourcemanager.WorkflowGasResourceType(421614)) - fee, _ := new(big.Int).SetString("123456789012345678901234", 10) - - t.Run("emits one gas record keyed by tx hash and logs the contract line", func(t *testing.T) { - lggr, obs := logger.TestObserved(t, zapcore.InfoLevel) - emitter := &recordingEmitter{} - rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) - - emitGasUsage(t.Context(), lggr, rm, identity, 421614, metadata, "0xabc", fee) - - require.Len(t, emitter.records, 1) - rec := emitter.records[0] - require.Equal(t, meteringpb.MeterAction_METER_ACTION_USAGE, rec.GetAction()) - require.Equal(t, resourcemanager.EmittingServiceChainWrite, rec.GetIdentity().GetService()) - require.Equal(t, "cre:workflow:gas", rec.GetIdentity().GetResourcePool()) - require.Equal(t, "cre:workflow:gas:421614", rec.GetIdentity().GetResourcePoolId()) - require.Len(t, rec.GetUtilizations(), 1) - u := rec.GetUtilizations()[0] - require.Equal(t, "cre:workflow:gas:421614", u.GetResourceType()) - require.Equal(t, "wf-1:exec-1", u.GetResourceId()) - require.Equal(t, "0xabc", u.GetEventId()) - require.Equal(t, "org-1", u.GetOrgId()) - require.Equal(t, "123456789012345678901234", u.GetValue()) - - logs := obs.FilterMessage("Emitted capability usage meter record").All() - require.Len(t, logs, 1) - fields := logs[0].ContextMap() - require.Equal(t, "exec-1", fields["executionID"]) - require.Equal(t, "0xabc", fields["eventID"]) - require.Equal(t, "cre:workflow:gas:421614", fields["resourceType"]) - require.Equal(t, "123456789012345678901234", fields["value"]) - require.Equal(t, "org-1", fields["orgID"]) - }) - - t.Run("no-op without a meter or fee", func(t *testing.T) { - lggr := logger.Test(t) - emitter := &recordingEmitter{} - rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) - emitGasUsage(t.Context(), lggr, nil, identity, 1, metadata, "0xabc", fee) - emitGasUsage(t.Context(), lggr, rm, identity, 1, metadata, "0xabc", nil) - require.Empty(t, emitter.records) - }) - - t.Run("refuses a malformed resource id instead of emitting a bad record", func(t *testing.T) { - lggr, obs := logger.TestObserved(t, zapcore.ErrorLevel) - emitter := &recordingEmitter{} - rm := resourcemanager.NewResourceManager(lggr, resourcemanager.ResourceManagerConfig{MeterRecordsEnabled: true, Emitter: emitter}) - emitGasUsage(t.Context(), lggr, rm, identity, 1, capabilities.RequestMetadata{WorkflowID: "", WorkflowExecutionID: "exec-1"}, "0xabc", fee) - require.Empty(t, emitter.records) - require.Len(t, obs.FilterMessage("Gas usage meter record not emitted").All(), 1) - }) -} diff --git a/chain_capabilities/solana/actions/write_report.go b/chain_capabilities/solana/actions/write_report.go index 08d2e5c49..0d6043618 100644 --- a/chain_capabilities/solana/actions/write_report.go +++ b/chain_capabilities/solana/actions/write_report.go @@ -22,7 +22,6 @@ import ( commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/contexts" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/resourcemanager" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" "github.com/smartcontractkit/chainlink-common/pkg/types" soltypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/solana" @@ -31,6 +30,7 @@ import ( ocrtypes "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" + "github.com/smartcontractkit/capabilities/chain_capabilities/common/gasmeter" ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" "github.com/smartcontractkit/capabilities/chain_capabilities/solana/metering" "github.com/smartcontractkit/capabilities/chain_capabilities/solana/monitoring" @@ -71,8 +71,7 @@ type WriteReport struct { reportSizeLimit limits.BoundLimiter[commoncfg.Size] transmissionScheduler ts.TransmissionScheduler - usageMeter *resourcemanager.ResourceManager - usageIdentity resourcemanager.ResourceIdentity + gasMeter *gasmeter.Meter } func (s *Solana) WriteReport( @@ -127,8 +126,7 @@ func (s *Solana) executeWriteReport(ctx context.Context, request *solcap.WriteRe beholderProcessor: s.beholderProcessor, messageBuilder: s.messageBuilder, transmissionScheduler: s.transmissionScheduler, - usageMeter: s.usageMeter, - usageIdentity: s.usageIdentity, + gasMeter: s.gasMeter, } return wr.executeWriteReport(ctx, request, telemetryContext, metadata) @@ -625,41 +623,10 @@ func (wr *WriteReport) meteringFromTxSignature(ctx context.Context, telemetryCon monitoring.LogAndEmitError(ctx, wr.lggr, wr.beholderProcessor, wr.messageBuilder.BuildWriteReportTxFeeCalculationError(telemetryContext, request, sig, err.Error())) return capabilities.ResponseMetadata{} } - emitGasUsage(ctx, wr.lggr, wr.usageMeter, wr.usageIdentity, wr.chainSelector, telemetryContext.RequestMetadata, sig.String(), new(big.Int).SetUint64(feeInLamports)) + wr.gasMeter.Emit(ctx, telemetryContext.RequestMetadata, sig.String(), new(big.Int).SetUint64(feeInLamports)) return metering.GetResponseMetadataWriteReport(feeInLamports, wr.chainSelector) } -// emitGasUsage emits the cre:workflow:gas: usage MeterRecord for -// one chain write and logs the emission. The log line is a contract consumed by -// the billing reconciler (fields: executionID, eventID, resourceType, value, -// orgID, txHash) and must stay stable. Fail-open: never affects the reply. -func emitGasUsage(ctx context.Context, lggr logger.Logger, rm *resourcemanager.ResourceManager, identity resourcemanager.ResourceIdentity, chainSelector uint64, metadata capabilities.RequestMetadata, txHash string, fee *big.Int) { - if rm == nil || fee == nil { - return - } - resourceID, err := resourcemanager.WorkflowUsageResourceID(metadata.WorkflowID, metadata.WorkflowExecutionID) - if err != nil { - lggr.Errorw("Gas usage meter record not emitted", "err", err, "executionID", metadata.WorkflowExecutionID) - return - } - resourceType := resourcemanager.WorkflowGasResourceType(chainSelector) - // The capability event id for gas is the transaction hash: one record per - // on-chain write, identical on every node of the DON that observes it. - rm.EmitUsageValue(ctx, identity, txHash, fee, resourcemanager.UtilizationFields{ - ResourceType: resourceType, - ResourceID: resourceID, - OrgID: metadata.OrgID, - }) - lggr.Infow("Emitted capability usage meter record", - "executionID", metadata.WorkflowExecutionID, - "eventID", txHash, - "resourceType", resourceType, - "value", fee.String(), - "orgID", metadata.OrgID, - "txHash", txHash, - ) -} - func (wr *WriteReport) getFee(ctx context.Context, sig solana.Signature) (uint64, error) { tx, err := wr.GetTransaction(ctx, soltypes.GetTransactionRequest{Signature: soltypes.Signature(sig)}) if err != nil { diff --git a/chain_capabilities/solana/go.mod b/chain_capabilities/solana/go.mod index 83e84f1ee..8f9ee2206 100644 --- a/chain_capabilities/solana/go.mod +++ b/chain_capabilities/solana/go.mod @@ -6,7 +6,7 @@ require ( github.com/gagliardetto/solana-go v1.23.0 github.com/google/go-cmp v0.7.0 github.com/mr-tron/base58 v1.2.0 - github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 + github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 github.com/smartcontractkit/chain-selectors v1.0.104 github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464 diff --git a/chain_capabilities/solana/go.sum b/chain_capabilities/solana/go.sum index 141752115..15831dab7 100644 --- a/chain_capabilities/solana/go.sum +++ b/chain_capabilities/solana/go.sum @@ -624,6 +624,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 h1:nJ3V1j6QSFJ3zfwTO8Ty5TVNEb7g4HUkr9VXXV7hLSg= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866/go.mod h1:URIXl+ew48QapONMSLC1faCcGRnm5CRL3jj1PtpJt5E= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 h1:I3k5OaOI3BwdFH0GCIcibPnKD6HaxngU6z6bppPCx8g= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 h1:2+0wOWEiYcEm0XlS1HwgH09y/3qZ0zjokTdbExY2VQQ= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961/go.mod h1:h4kazMLsDM6CsKl2OnT9AtCHhhRsFptA/OTM7Hkj2GE= github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= diff --git a/chain_capabilities/solana/main.go b/chain_capabilities/solana/main.go index 802798c96..852742cc4 100644 --- a/chain_capabilities/solana/main.go +++ b/chain_capabilities/solana/main.go @@ -27,6 +27,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/core" capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common" + "github.com/smartcontractkit/capabilities/chain_capabilities/common/gasmeter" ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule" "github.com/smartcontractkit/capabilities/chain_capabilities/solana/actions" "github.com/smartcontractkit/capabilities/chain_capabilities/solana/config" @@ -58,10 +59,10 @@ type capabilityGRPCService struct { triggerService *trigger.SolanaLogTriggerService // meteringCfg comes from the LOOP environment ([Metering] on the host). - // usageMeter emits cre:workflow:gas usage records for write reports when + // gasMeter emits cre:workflow:gas usage records for write reports when // MeterRecordsEnabled; nil otherwise. meteringCfg resourcemanager.Config - usageMeter *resourcemanager.ResourceManager + gasMeter *gasmeter.Meter } type capability struct { @@ -120,8 +121,8 @@ func (c *capabilityGRPCService) Close() error { if c.Solana != nil { closers = append(closers, c.Solana) } - if c.usageMeter != nil { - closers = append(closers, c.usageMeter) + if c.gasMeter != nil { + closers = append(closers, c.gasMeter) } return services.CloseAll(closers...) } @@ -254,19 +255,10 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor if err != nil { return err } - if c.meteringCfg.MeterRecordsEnabled { - rmCfg := c.meteringCfg.ResourceManagerConfig - rmCfg.MeterSnapshotsEnabled = false - c.usageMeter = resourcemanager.NewResourceManager(c.lggr, rmCfg) - identity := resourcemanager.WithWorkflowUsagePool(resourcemanager.NewBaseIdentity(c.meteringCfg.DeploymentIdentity, resourcemanager.EmittingServiceChainWrite, ""), resourcemanager.WorkflowGasResourceType(c.chainSelector)) - if dependencies.CapabilityDonID != 0 { - identity = identity.WithDonID(strconv.FormatUint(uint64(dependencies.CapabilityDonID), 10)) - } - c.Solana.WithUsageMeter(c.usageMeter, identity) - if rmCfg.Emitter == nil { - c.lggr.Errorw("Capability usage metering enabled but this LOOP has no durable emitter; gas usage records will not be delivered") - } - toStart = append(toStart, c.usageMeter) + c.gasMeter = gasmeter.New(c.lggr, c.meteringCfg, c.chainSelector, dependencies.CapabilityDonID) + c.Solana.WithGasMeter(c.gasMeter) + if c.gasMeter != nil { + toStart = append(toStart, c.gasMeter) } c.triggerService, err = trigger.NewLogTriggerService(trigger.LogTriggerServiceOpts{ From 55912d0129222ea8a4cbae47d0891f7a5e3980d4 Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Wed, 7 Oct 2026 12:15:14 -0400 Subject: [PATCH 6/7] chain-write: drop redundant embedded selector (QF1008), go mod tidy Co-Authored-By: Claude Fable 5.1 --- chain_capabilities/evm/go.mod | 2 -- chain_capabilities/evm/go.sum | 11 ----------- chain_capabilities/evm/main.go | 2 +- chain_capabilities/solana/go.sum | 6 ------ chain_capabilities/solana/main.go | 2 +- 5 files changed, 2 insertions(+), 21 deletions(-) diff --git a/chain_capabilities/evm/go.mod b/chain_capabilities/evm/go.mod index 40fb45ab8..1a127eb19 100644 --- a/chain_capabilities/evm/go.mod +++ b/chain_capabilities/evm/go.mod @@ -50,7 +50,6 @@ require ( 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/dominikbraun/graph v0.23.0 // indirect github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect @@ -207,7 +206,6 @@ require ( 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 - sigs.k8s.io/yaml v1.4.0 // indirect ) replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20250528121202-292529af39df diff --git a/chain_capabilities/evm/go.sum b/chain_capabilities/evm/go.sum index 60ea408a5..23fa0bd61 100644 --- a/chain_capabilities/evm/go.sum +++ b/chain_capabilities/evm/go.sum @@ -91,8 +91,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= -github.com/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= @@ -190,7 +188,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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= @@ -455,22 +452,16 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 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-20260922114021-5cee95428866 h1:nJ3V1j6QSFJ3zfwTO8Ty5TVNEb7g4HUkr9VXXV7hLSg= -github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866/go.mod h1:URIXl+ew48QapONMSLC1faCcGRnm5CRL3jj1PtpJt5E= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 h1:I3k5OaOI3BwdFH0GCIcibPnKD6HaxngU6z6bppPCx8g= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 h1:2+0wOWEiYcEm0XlS1HwgH09y/3qZ0zjokTdbExY2VQQ= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961/go.mod h1:h4kazMLsDM6CsKl2OnT9AtCHhhRsFptA/OTM7Hkj2GE= github.com/smartcontractkit/chain-selectors v1.0.112 h1:3VO+ttHsqGzPviMmb1AsIT5+rXVgz/Xx6wmvTpamrzE= github.com/smartcontractkit/chain-selectors v1.0.112/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260925190414-e1b87ad2d7b1 h1:1rGIv4vUt+Et7k5waRbkODOQsMuEakQ8TA9PiMBaPss= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260925190414-e1b87ad2d7b1/go.mod h1:UkZJ6uUU9Hlonfb+k62Pg4yRuvOsoShoOd4tPEB/qpE= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464 h1:DVXwyIGekGqLaFAcSWa3K3S209gAY0WDFwliYdLp1dk= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464/go.mod h1:37Mu0pbAnGmHPgOZ8bQvgP4hzXw/Wu7smKX4cN46UOs= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049 h1:nQ2bw8QQLT+AuCE6JUVIRT1RAo08wQHR9jWh+MgT2GA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260917115705-1d3a14a9b049/go.mod h1:FO9igruzwHPXstyb9J9z83sJ19iLSNSIg+0H/n0wlJU= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 h1:J4vYRJxkBO8Yf//17wScKjYWmS45VZaFWx0zt+4ETEU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260917132551-2a2b38e8b744 h1:n+V23NCROMeWFHUyR78Z4dXkUnPV3K8RzOzJpnXuXV0= @@ -843,5 +834,3 @@ 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= diff --git a/chain_capabilities/evm/main.go b/chain_capabilities/evm/main.go index 05eaa30ce..833218732 100644 --- a/chain_capabilities/evm/main.go +++ b/chain_capabilities/evm/main.go @@ -183,7 +183,7 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor return fmt.Errorf("failed to init evm relayer for chainID %d from relayer: %w", cfg.ChainID, err) } c.gasMeter = gasmeter.New(c.lggr, c.meteringCfg, c.chainSelector, capabilityDonID) - c.EVM.WithGasMeter(c.gasMeter) + c.WithGasMeter(c.gasMeter) // TODO: add org resolver capabilityID := fmt.Sprintf("%s (%d)", c.id, cfg.ChainID) diff --git a/chain_capabilities/solana/go.sum b/chain_capabilities/solana/go.sum index 15831dab7..9cfb29c01 100644 --- a/chain_capabilities/solana/go.sum +++ b/chain_capabilities/solana/go.sum @@ -622,8 +622,6 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866 h1:nJ3V1j6QSFJ3zfwTO8Ty5TVNEb7g4HUkr9VXXV7hLSg= -github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20260922114021-5cee95428866/go.mod h1:URIXl+ew48QapONMSLC1faCcGRnm5CRL3jj1PtpJt5E= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 h1:I3k5OaOI3BwdFH0GCIcibPnKD6HaxngU6z6bppPCx8g= github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 h1:2+0wOWEiYcEm0XlS1HwgH09y/3qZ0zjokTdbExY2VQQ= @@ -636,12 +634,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288/go.mod h1:jPHlo/IN2YAArI001JJixmm6ZHQwgnAVJXY8VBFiFTc= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 h1:eEjTgIQn4RW0ZPRepUDYTdgGwaRCMawMwgXkHItUc9U= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260925190414-e1b87ad2d7b1 h1:1rGIv4vUt+Et7k5waRbkODOQsMuEakQ8TA9PiMBaPss= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260925190414-e1b87ad2d7b1/go.mod h1:UkZJ6uUU9Hlonfb+k62Pg4yRuvOsoShoOd4tPEB/qpE= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464 h1:DVXwyIGekGqLaFAcSWa3K3S209gAY0WDFwliYdLp1dk= github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464/go.mod h1:37Mu0pbAnGmHPgOZ8bQvgP4hzXw/Wu7smKX4cN46UOs= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 h1:J4vYRJxkBO8Yf//17wScKjYWmS45VZaFWx0zt+4ETEU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340 h1:PsjEI+5jZIz9AS4eOsLS5VpSWJINf38clXV3wryPyMk= diff --git a/chain_capabilities/solana/main.go b/chain_capabilities/solana/main.go index 852742cc4..bc30fa81b 100644 --- a/chain_capabilities/solana/main.go +++ b/chain_capabilities/solana/main.go @@ -256,7 +256,7 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor return err } c.gasMeter = gasmeter.New(c.lggr, c.meteringCfg, c.chainSelector, dependencies.CapabilityDonID) - c.Solana.WithGasMeter(c.gasMeter) + c.WithGasMeter(c.gasMeter) if c.gasMeter != nil { toStart = append(toStart, c.gasMeter) } From 81bbbfabd8ceb5fb8bf1059cedac9a9b7f32567b Mon Sep 17 00:00:00 2001 From: Dylan Tinianov Date: Fri, 9 Oct 2026 14:53:12 -0400 Subject: [PATCH 7/7] chain-write: pin chain_capabilities/common to main (gasmeter #813 merged) Co-Authored-By: Claude Fable 5.1 --- chain_capabilities/evm/go.mod | 2 +- chain_capabilities/evm/go.sum | 4 ++-- chain_capabilities/solana/go.mod | 2 +- chain_capabilities/solana/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chain_capabilities/evm/go.mod b/chain_capabilities/evm/go.mod index 1a127eb19..6b1cb9d9e 100644 --- a/chain_capabilities/evm/go.mod +++ b/chain_capabilities/evm/go.mod @@ -5,7 +5,7 @@ go 1.27.1 require ( github.com/ethereum/go-ethereum v1.17.4 github.com/google/go-cmp v0.7.0 - github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 + github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261009182120-372f846f528c github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 github.com/smartcontractkit/chain-selectors v1.0.112 github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464 diff --git a/chain_capabilities/evm/go.sum b/chain_capabilities/evm/go.sum index 23fa0bd61..66d66b550 100644 --- a/chain_capabilities/evm/go.sum +++ b/chain_capabilities/evm/go.sum @@ -452,8 +452,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 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-20261007160123-bed53f59b8a4 h1:I3k5OaOI3BwdFH0GCIcibPnKD6HaxngU6z6bppPCx8g= -github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261009182120-372f846f528c h1:R8cKOczBWJ31Spy7ii/+Im269YeWbk6oJ33vDd6cJ3Q= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261009182120-372f846f528c/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 h1:2+0wOWEiYcEm0XlS1HwgH09y/3qZ0zjokTdbExY2VQQ= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961/go.mod h1:h4kazMLsDM6CsKl2OnT9AtCHhhRsFptA/OTM7Hkj2GE= github.com/smartcontractkit/chain-selectors v1.0.112 h1:3VO+ttHsqGzPviMmb1AsIT5+rXVgz/Xx6wmvTpamrzE= diff --git a/chain_capabilities/solana/go.mod b/chain_capabilities/solana/go.mod index 8f9ee2206..6670b8f4b 100644 --- a/chain_capabilities/solana/go.mod +++ b/chain_capabilities/solana/go.mod @@ -6,7 +6,7 @@ require ( github.com/gagliardetto/solana-go v1.23.0 github.com/google/go-cmp v0.7.0 github.com/mr-tron/base58 v1.2.0 - github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 + github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261009182120-372f846f528c github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 github.com/smartcontractkit/chain-selectors v1.0.104 github.com/smartcontractkit/chainlink-common v0.11.2-0.20261006175240-29594528f464 diff --git a/chain_capabilities/solana/go.sum b/chain_capabilities/solana/go.sum index 9cfb29c01..611a9f60b 100644 --- a/chain_capabilities/solana/go.sum +++ b/chain_capabilities/solana/go.sum @@ -622,8 +622,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4 h1:I3k5OaOI3BwdFH0GCIcibPnKD6HaxngU6z6bppPCx8g= -github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261007160123-bed53f59b8a4/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261009182120-372f846f528c h1:R8cKOczBWJ31Spy7ii/+Im269YeWbk6oJ33vDd6cJ3Q= +github.com/smartcontractkit/capabilities/chain_capabilities/common v0.0.0-20261009182120-372f846f528c/go.mod h1:ubztvmJnKJilCw4CzrlHSkKvPYXNeUArML/xY1VzsBg= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961 h1:2+0wOWEiYcEm0XlS1HwgH09y/3qZ0zjokTdbExY2VQQ= github.com/smartcontractkit/capabilities/libs v0.0.0-20261001142216-b1dea8dbb961/go.mod h1:h4kazMLsDM6CsKl2OnT9AtCHhhRsFptA/OTM7Hkj2GE= github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM=