Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions chain_capabilities/evm/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type EVM struct {
keystoneForwarderAddress common.Address
forwarderClient contracts.CREForwarderClient
ReceiverGasMinimum uint64
forwarderGasOverhead uint64
LookbackBlocks uint64

lggr logger.SugaredLogger
Expand Down Expand Up @@ -80,6 +81,7 @@ func NewEVM(cfg config.Config, evmService types.EVMService, lggr logger.Logger,
keystoneForwarderAddress: keystoneForwarderAddress,
forwarderClient: kfc,
ReceiverGasMinimum: cfg.ReceiverGasMinimum,
forwarderGasOverhead: contracts.ForwarderGasOverhead(cfg.ForwarderGasOverheadMargin),
lggr: logger.Sugared(lggr),
beholderProcessor: beholderProcessor,
messageBuilder: messageBuilder,
Expand Down
164 changes: 126 additions & 38 deletions chain_capabilities/evm/actions/write_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@

type WriteReport struct {
types.EVMService
forwarderClient contracts.CREForwarderClient
ReceiverGasMinimum uint64
chainSelector uint64
forwarderClient contracts.CREForwarderClient
ReceiverGasMinimum uint64
forwarderGasOverhead uint64
chainSelector uint64

lggr logger.SugaredLogger
beholderProcessor beholder.ProtoProcessor
Expand Down Expand Up @@ -83,10 +84,11 @@

func (e *EVM) executeWriteReport(ctx context.Context, request *evm.WriteReportRequest, metadata capabilities.RequestMetadata, telemetryContext monitoring.TelemetryContext) (*evm.WriteReportReply, capabilities.ResponseMetadata, error) {
wr := &WriteReport{
EVMService: e.EVMService,
forwarderClient: e.forwarderClient,
ReceiverGasMinimum: e.ReceiverGasMinimum,
chainSelector: e.chainSelector,
EVMService: e.EVMService,
forwarderClient: e.forwarderClient,
ReceiverGasMinimum: e.ReceiverGasMinimum,
forwarderGasOverhead: e.forwarderGasOverhead,
chainSelector: e.chainSelector,

lggr: e.messageBuilder.RequestLggr(e.lggr, telemetryContext),
beholderProcessor: e.beholderProcessor,
Expand All @@ -102,7 +104,7 @@
return wr.executeWriteReport(ctx, request, metadata, telemetryContext)
}

func (e *WriteReport) executeWriteReport(ctx context.Context, request *evm.WriteReportRequest, metadata capabilities.RequestMetadata, telemetryContext monitoring.TelemetryContext) (*evm.WriteReportReply, capabilities.ResponseMetadata, error) {

Check warning on line 107 in chain_capabilities/evm/actions/write_report.go

View check run for this annotation

CL-sonarqube-production / SonarQube Code Analysis

Refactor this method to reduce its Cognitive Complexity from 56 to the 30 allowed.

[S3776] Cognitive Complexity of functions should not be too high See more on https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_capabilities&pullRequest=766&issues=9b3f15d6-665b-4c2f-b596-cd301d986162&open=9b3f15d6-665b-4c2f-b596-cd301d986162
transmissionID, err := getTransmissionID(metadata.WorkflowExecutionID, request)
if err != nil {
return nil, capabilities.ResponseMetadata{}, err
Expand All @@ -110,6 +112,7 @@
e.lggr = e.lggr.With(transmissionID.LogAttrs()...)

ctx = contexts.WithChainSelector(ctx, e.chainSelector)
userProvidedGas := request.GasConfig != nil && request.GasConfig.GasLimit != 0
if request.GasConfig == nil || request.GasConfig.GasLimit == 0 {
request.GasConfig = &evm.GasConfig{}
request.GasConfig.GasLimit, err = e.txGasLimit.Limit(ctx)
Expand Down Expand Up @@ -152,7 +155,7 @@
txHash, err := txHashRetriever.GetFailedTransmissionHash(ctx)
if err != nil {
if errors.Is(err, ErrUnexpectedSuccessfulTransmission) {
monitoring.LogAndEmitError(ctx, e.lggr, e.beholderProcessor, e.messageBuilder.BuildWriteReportInvalidTransmissionState(telemetryContext, request, transmissionInfo, "WriteReport unexpected successful transmission", err.Error()))

Check warning on line 158 in chain_capabilities/evm/actions/write_report.go

View check run for this annotation

CL-sonarqube-production / SonarQube Code Analysis

Define a constant instead of duplicating this literal "WriteReport unexpected successful transmission" 3 times.

[S1192] String literals should not be duplicated See more on https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_capabilities&pullRequest=766&issues=54abab9d-dd47-4bfd-9482-eebd7e7ca084&open=54abab9d-dd47-4bfd-9482-eebd7e7ca084
} else {
e.lggr.Errorw("Returning without a transmission attempt - prior transmission marked receiver invalid, but failed to retrieve its tx hash")
}
Expand All @@ -168,24 +171,16 @@
}
return reply, e.meteringFromReply(reply), nil
case contracts.TransmissionStateFailed:
hadEnoughGas, calculatedReceiverGasBudget := e.attemptHadEnoughGas(request, transmissionInfo)
if hadEnoughGas {
txHash, err := txHashRetriever.GetFailedTransmissionHash(ctx)
if err != nil {
if errors.Is(err, ErrUnexpectedSuccessfulTransmission) {
monitoring.LogAndEmitError(ctx, e.lggr, e.beholderProcessor, e.messageBuilder.BuildWriteReportInvalidTransmissionState(telemetryContext, request, transmissionInfo, "WriteReport unexpected successful transmission", err.Error()))
} else {
e.lggr.Errorw("Returning without a transmission attempt - prior transmission failed with sufficient gas, but failed to retrieve its tx hash", "error", err.Error(), "receiverGasBudget", calculatedReceiverGasBudget, "transmissionReceiverGasBudget", transmissionInfo.GasLimit)
}
return nil, capabilities.ResponseMetadata{}, err
}

decision, err := e.assessFailedTransmission(ctx, request, transmissionInfo, txHashRetriever, telemetryContext, userProvidedGas)
if err != nil {
return nil, capabilities.ResponseMetadata{}, err
}
if !decision.retry {
e.lggr.Infow("Returning without a transmission attempt - prior transmission failed with sufficient gas",
"txHash", common.Bytes2Hex(txHash[:]),
"receiverGasBudget", calculatedReceiverGasBudget,
"receiverGasBudget", decision.receiverGasBudget,
"transmissionReceiverGasBudget", transmissionInfo.GasLimit,
)
reply, err := e.buildRevertReplyFromTx(ctx, *txHash, transmissionInfo, transmissionID)
reply, err := e.buildRevertReplyFromTx(ctx, decision.priorTxHash, transmissionInfo, transmissionID)
if err != nil {
// The receiver reverted despite sufficient gas (user contract fault); surface the
// reason even if the receipt/fee lookup failed, as a user error.
Expand All @@ -194,7 +189,7 @@
return reply, e.meteringFromReply(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))
e.messageBuilder.BuildWriteReportInsufficientGasRetry(telemetryContext, request, decision.receiverGasBudget, transmissionInfo.GasLimit, queuePosition))
default:
errorMsg := getInvalidStateErrorMessage(transmissionInfo.State)
monitoring.LogAndEmitError(ctx, e.lggr, e.beholderProcessor, e.messageBuilder.BuildWriteReportInvalidTransmissionState(telemetryContext, request, transmissionInfo, "WriteReport invalid transmission state", errorMsg))
Expand Down Expand Up @@ -292,7 +287,7 @@
}

// pollTransmissionInfo returns final state of the transmission at this point of the transmission schedule, taking into account previous nodes in the queue.
func (e *WriteReport) pollTransmissionInfo(

Check warning on line 290 in chain_capabilities/evm/actions/write_report.go

View check run for this annotation

CL-sonarqube-production / SonarQube Code Analysis

Refactor this method to reduce its Cognitive Complexity from 32 to the 30 allowed.

[S3776] Cognitive Complexity of functions should not be too high See more on https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_capabilities&pullRequest=766&issues=39173070-7741-40fb-a2fd-8c5619f87178&open=39173070-7741-40fb-a2fd-8c5619f87178
ctx context.Context,
request *evm.WriteReportRequest,
telemetryContext monitoring.TelemetryContext,
Expand Down Expand Up @@ -347,9 +342,10 @@
case contracts.TransmissionStateSucceeded, contracts.TransmissionStateInvalidReceiver:
return lastValidInfo, nil
case contracts.TransmissionStateFailed:
hadEnoughGas, calculatedReceiverGasBudget := e.attemptHadEnoughGas(request, lastValidInfo)
// none of the previous nodes will try to resend this transmission, so we can stop polling early
if hadEnoughGas {
// Cheap estimate check while polling; the authoritative decision is made after
// polling completes (see assessFailedTransmission).
if e.recordedBudgetExceedsEstimate(request, lastValidInfo) {
// none of the previous nodes will try to resend this transmission, so we can stop polling early
return lastValidInfo, nil
}
_, count, err := txHashRetriever.GetFailedTransmissionHashWithCount(ctx)
Expand All @@ -359,7 +355,6 @@
e.lggr.Infow("Stopping poll - all prior nodes in queue finished their transmission attempts",
"queuePosition", queuePosition,
"failedAttemptCount", count,
"calculatedReceiverGasBudget", calculatedReceiverGasBudget,
"transmissionReceiverGasBudget", lastValidInfo.GasLimit,
)
return lastValidInfo, nil
Expand Down Expand Up @@ -416,18 +411,111 @@
return fmt.Sprintf("unexpected transmission state: %v", state)
}

// attemptHadEnoughGas reports whether a prior failed transmission used sufficient receiver gas,
// meaning a resubmit would not help (e.g. receiver contract revert).
// The second return value is the receiver gas budget derived from the request.
func (e *WriteReport) attemptHadEnoughGas(request *evm.WriteReportRequest, info contracts.TransmissionInfo) (bool, uint64) {
receiverGasBudget := e.ReceiverGasMinimum + contracts.ForwarderContractLogicGasCost
// failedTransmissionDecision is the outcome of assessing a failed transmission: either the
// failure is genuine (retry=false, priorTxHash identifies the failed attempt) or a retry can
// deliver more receiver gas than the failed attempt got (retry=true).
type failedTransmissionDecision struct {
retry bool
// priorTxHash is the failed attempt's tx hash, set when retry=false.
priorTxHash evmtypes.Hash
// receiverGasBudget is the offchain-derived receiver gas budget, for telemetry.
receiverGasBudget uint64
}

// assessFailedTransmission decides whether a failed transmission should be retried.
//
// The verdict answers one question: would our resubmission deliver more receiver gas than
// the failed attempt got? A retry sends the identical report through the identical forwarder,
// so the forwarder overhead cancels out and the answer reduces to comparing gas limits.
//
// When the user provided the gas limit, the comparison is trustless on both sides: the prior
// tx's actual onchain gas limit (consensus data the previous submitter cannot misreport)
// against the user's requested limit (what every honest node would have submitted). Any
// inequality — lower or higher — is anomalous and emits a warn log plus a gas mismatch metric.
//
// When the gas limit was node-derived, or the prior tx's gas cannot be fetched, the comparison
// falls back to the forwarder-recorded receiver budget against our offchain estimate.
func (e *WriteReport) assessFailedTransmission(ctx context.Context, request *evm.WriteReportRequest, transmissionInfo contracts.TransmissionInfo, txHashRetriever TxHashRetriever, telemetryContext monitoring.TelemetryContext, userProvidedGas bool) (failedTransmissionDecision, error) {
receiverGasBudget := e.estimateReceiverGasBudget(request)

priorTxHash, err := txHashRetriever.GetFailedTransmissionHash(ctx)
if err != nil {
if errors.Is(err, ErrUnexpectedSuccessfulTransmission) {
monitoring.LogAndEmitError(ctx, e.lggr, e.beholderProcessor, e.messageBuilder.BuildWriteReportInvalidTransmissionState(telemetryContext, request, transmissionInfo, "WriteReport unexpected successful transmission", err.Error()))
} else {
e.lggr.Errorw("Failed to retrieve the prior failed transmission's tx hash", "error", err.Error(), "receiverGasBudget", receiverGasBudget, "transmissionReceiverGasBudget", transmissionInfo.GasLimit)
}
return failedTransmissionDecision{}, err
}

if userProvidedGas {
priorTxGas, gasErr := e.fetchTxGasLimit(ctx, *priorTxHash)
if gasErr == nil {
requestedGasLimit := request.GasConfig.GetGasLimit()
if priorTxGas != requestedGasLimit {
e.warnGasMismatch(ctx, telemetryContext, request, *priorTxHash, requestedGasLimit, priorTxGas)
}
return failedTransmissionDecision{
retry: priorTxGas < requestedGasLimit,
priorTxHash: *priorTxHash,
receiverGasBudget: receiverGasBudget,
}, nil
}
e.lggr.Debugw("Failed to fetch the prior failed transmission tx's gas limit, falling back to estimate comparison", "error", gasErr)
}

// Node-derived gas, or the prior tx's gas could not be fetched: compare the forwarder-
// recorded receiver budget against the offchain estimate. A nil recorded budget is retryable.
if transmissionInfo.GasLimit == nil || transmissionInfo.GasLimit.Uint64() <= receiverGasBudget {
return failedTransmissionDecision{retry: true, receiverGasBudget: receiverGasBudget}, nil
}
return failedTransmissionDecision{priorTxHash: *priorTxHash, receiverGasBudget: receiverGasBudget}, nil
}

// estimateReceiverGasBudget derives the receiver gas budget offchain: the requested gas
// limit minus the forwarder's gas overhead, or the configured receiver gas minimum when no
// explicit limit was provided.
func (e *WriteReport) estimateReceiverGasBudget(request *evm.WriteReportRequest) uint64 {
receiverGasBudget := e.ReceiverGasMinimum + e.forwarderGasOverhead

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come we add forwarderGasOverhead to receiverGasBudget here ?
i might be missing something.
from my understanding, receiverGasBudget is the gas that the receiver has available to use. Not being able to understand why we add forwarderGas into that ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two options:

  1. User did not provide Gas, then the value we provide is forwarderGasOverhead + receiverGasMinimum. Because we want to cover our internal forwarder logic + some gas for receiver execution(the constants are aligned with what we have on chain)
  2. User provided gas, further in the code you can see calculations for this case

if request.GasConfig != nil && request.GasConfig.GasLimit > receiverGasBudget {
receiverGasBudget = request.GasConfig.GasLimit - contracts.ForwarderContractLogicGasCost
receiverGasBudget = request.GasConfig.GasLimit - e.forwarderGasOverhead
Comment on lines -425 to +481

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this again, shouldn't we keep contracts.ForwarderContractLogicGasCost around for backwards compatibility? Otherwise forwarderGasOverhead won't include a ForwarderGasOverheadMargin since this change introduces that field.

The other option is separately introducing ForwarderGasOverheadMargin, setting it on whichever chains need it, then adding this new logic to replace contracts.ForwarderContractLogicGasCost

}
return receiverGasBudget
}

// recordedBudgetExceedsEstimate reports whether the forwarder-recorded receiver budget
// exceeds our offchain estimate, i.e. the failed attempt plausibly had enough gas. Used only
// as the polling early-exit heuristic; a nil recorded budget counts as not enough.
func (e *WriteReport) recordedBudgetExceedsEstimate(request *evm.WriteReportRequest, transmissionInfo contracts.TransmissionInfo) bool {
if transmissionInfo.GasLimit == nil {
return false
}
if info.GasLimit == nil {
return false, receiverGasBudget
return transmissionInfo.GasLimit.Uint64() > e.estimateReceiverGasBudget(request)
}

// warnGasMismatch reports a prior tx whose onchain gas limit differs from the requested one.
func (e *WriteReport) warnGasMismatch(ctx context.Context, telemetryContext monitoring.TelemetryContext, request *evm.WriteReportRequest, txHash evmtypes.Hash, requestedGasLimit, actualTxGasLimit uint64) {
e.lggr.Warnw("Gas mismatch: prior transmission tx gas limit does not match the requested gas limit",
Comment thread
amit-momin marked this conversation as resolved.
"txHash", common.Bytes2Hex(txHash[:]),
"priorTxGasLimit", actualTxGasLimit,
"requestedGasLimit", requestedGasLimit,
)
monitoring.EmitInitiated(ctx, e.lggr, e.beholderProcessor,
e.messageBuilder.BuildWriteReportGasMismatch(telemetryContext, request, common.Bytes2Hex(txHash[:]), requestedGasLimit, actualTxGasLimit))
}

// fetchTxGasLimit reads a transaction's gas limit from chain.
func (e *WriteReport) fetchTxGasLimit(ctx context.Context, txHash evmtypes.Hash) (uint64, error) {
tx, err := capcommon.WithQuickRetry(ctx, e.lggr, func(ctx context.Context) (*evmtypes.Transaction, error) {
return e.GetTransactionByHash(ctx, evmtypes.GetTransactionByHashRequest{
Hash: txHash,
IsExternal: false, // gas limit is needed for the retry decision, not user output
})
})
if err != nil {
return 0, err
}
return info.GasLimit.Uint64() > receiverGasBudget, receiverGasBudget
return tx.Gas, nil
}

func getTransmissionID(workflowExecutionID string, request *evm.WriteReportRequest) (contracts.TransmissionID, error) {
Expand Down Expand Up @@ -566,8 +654,8 @@
return err
}

if request.GasConfig != nil && request.GasConfig.GasLimit != 0 && request.GasConfig.GasLimit < e.ReceiverGasMinimum+contracts.ForwarderContractLogicGasCost {
return fmt.Errorf("gas limit is %d, which is lower than minimum gas limit of: %d, for unbounded gas leave the gas limit as nil or 0", request.GasConfig.GasLimit, e.ReceiverGasMinimum+contracts.ForwarderContractLogicGasCost)
if request.GasConfig != nil && request.GasConfig.GasLimit != 0 && request.GasConfig.GasLimit < e.ReceiverGasMinimum+e.forwarderGasOverhead {
return fmt.Errorf("gas limit is %d, which is lower than minimum gas limit of: %d, for unbounded gas leave the gas limit as nil or 0", request.GasConfig.GasLimit, e.ReceiverGasMinimum+e.forwarderGasOverhead)
}

return nil
Expand Down
Loading
Loading