Skip to content
Merged
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: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ require (
github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872
github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1
github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260325164211-c77e73c79080
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57
github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251020150604-8ab84f7bad1a
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -660,8 +660,8 @@ github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8
github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1/go.mod h1:oyfOm4k0uqmgZIfxk1elI/59B02shbbJQiiUdPdbMgI=
github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 h1:ACpDbAxG4fa4sA83dbtYcrnlpE/y7thNIZfHxTv2ZLs=
github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563/go.mod h1:jP5mrOLFEYZZkl7EiCHRRIMSSHCQsYypm1OZSus//iI=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260325164211-c77e73c79080 h1:H1VUXAOzhPOSTQdLHs+eI75SBEjBDwqUkmZPHb6cQ2c=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260325164211-c77e73c79080/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57 h1:sCrr1Oy/JZstf/Oi2cRuU4mDN1BRUKfXP2CKByCMADg=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs=
github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251020150604-8ab84f7bad1a h1:pr0VFI7AWlDVJBEkcvzXWd97V8w8QMNjRdfPVa/IQLk=
github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251020150604-8ab84f7bad1a/go.mod h1:jo+cUqNcHwN8IF7SInQNXDZ8qzBsyMpnLdYbDswviFc=
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 h1:T/eCDsUI8EJT4n5zSP4w1mz4RHH+ap8qieA17QYfBhk=
Expand Down
28 changes: 23 additions & 5 deletions pkg/transmitter/dual_contract_transmitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package transmitter
import (
"context"
"database/sql"
"encoding/hex"
stderrors "errors"
"fmt"
"strings"
Expand All @@ -20,6 +19,7 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-evm/pkg/keys"
"github.com/smartcontractkit/chainlink-evm/pkg/logpoller"
"github.com/smartcontractkit/chainlink-evm/pkg/txmgr"
)

// TODO: Remove when new dual transmitter contracts are merged
Expand Down Expand Up @@ -120,7 +120,14 @@ func (oc *dualContractTransmitter) Transmit(ctx context.Context, reportCtx ocrty
oc.lggr.Warnw("failed to generate tx metadata for report", "err", err)
}

oc.lggr.Debugw("Transmitting report", "report", hex.EncodeToString(report), "rawReportCtx", rawReportCtx, "contractAddress", oc.contractAddress, "txMeta", txMeta)
if txMeta == nil {
txMeta = &txmgr.TxMeta{}
}
transactionLifecycleID := generateTransactionLifecycleIDForOCR2(reportCtx.ReportTimestamp)
txMeta.TransactionLifecycleID = &transactionLifecycleID
oc.lggr.Infow("Transmitting report", "configDigest", "0x"+reportCtx.ReportTimestamp.ConfigDigest.Hex(), "epoch", reportCtx.ReportTimestamp.Epoch, "round", reportCtx.ReportTimestamp.Round, "contractAddress",
oc.contractAddress, "txMeta", txMeta, "transactionLifecycleID", transactionLifecycleID)


// Primary transmission
payload, err := oc.contractABI.Pack("transmit", rawReportCtx, []byte(report), rs, ss, vs)
Expand All @@ -129,8 +136,11 @@ func (oc *dualContractTransmitter) Transmit(ctx context.Context, reportCtx ocrty
}

transactionErr := errors.Wrap(oc.transmitter.CreateEthTransaction(ctx, oc.contractAddress, payload, txMeta), "failed to send primary Eth transaction")

oc.lggr.Debugw("Created primary transaction", "error", transactionErr)
if transactionErr != nil {
oc.lggr.Errorw("Failed to create primary Eth transaction", "error", transactionErr, "transactionLifecycleID", transactionLifecycleID)
} else {
oc.lggr.Debugw("Created primary transaction", "transactionLifecycleID", transactionLifecycleID)
}

// Secondary transmission
secondaryPayload, err := oc.dualTransmissionABI.Pack("transmitSecondary", rawReportCtx, []byte(report), rs, ss, vs)
Expand All @@ -139,7 +149,11 @@ func (oc *dualContractTransmitter) Transmit(ctx context.Context, reportCtx ocrty
}

err = errors.Wrap(oc.transmitter.CreateSecondaryEthTransaction(ctx, secondaryPayload, txMeta), "failed to send secondary Eth transaction")
oc.lggr.Debugw("Created secondary transaction", "error", err)
if err != nil {
oc.lggr.Errorw("Failed to create secondary Eth transaction", "error", err, "transactionLifecycleID", transactionLifecycleID)
} else {
oc.lggr.Debugw("Created secondary transaction", "transactionLifecycleID", transactionLifecycleID)
}
return stderrors.Join(transactionErr, err)
}

Expand Down Expand Up @@ -242,6 +256,10 @@ func (oc *dualContractTransmitter) lockSecondary(ctx context.Context) error {
return nil
}

func generateTransactionLifecycleIDForOCR2(reportTimestamp ocrtypes.ReportTimestamp) string {
return fmt.Sprintf("0x%s:%d:%d", reportTimestamp.ConfigDigest.Hex(), reportTimestamp.Epoch, reportTimestamp.Round)
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.

the config digest captures the aggregator address right?

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.

Not directly, but it's encapsulated.

}

func (oc *dualContractTransmitter) Start(ctx context.Context) error {
return oc.lockTransmitters(ctx)
}
Expand Down
14 changes: 14 additions & 0 deletions pkg/transmitter/dual_contract_transmitter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@ func Test_dualContractTransmitter_Transmit_SignaturesAreTransmitted(t *testing.T
require.Equal(t, transmitter.lastSecondaryPayload, withSignaturesPayloadSecondary, "secondary payload not equal")
}

func Test_generateTracingIDForOCR2(t *testing.T) {
t.Parallel()

digestBytes, err := hex.DecodeString("000130da6b9315bd59af6b0a3f5463c0d0a39e92eaa34cbcbdbace7b3bfcc776")
require.NoError(t, err)

reportTimestamp := types.ReportTimestamp{Epoch: 42, Round: 7}
copy(reportTimestamp.ConfigDigest[:], digestBytes)

transactionLifecycleID := generateTransactionLifecycleIDForOCR2(reportTimestamp)

assert.Equal(t, "0x000130da6b9315bd59af6b0a3f5463c0d0a39e92eaa34cbcbdbace7b3bfcc776:42:7", transactionLifecycleID)
}

func createDualContractTransmitter(ctx context.Context, t *testing.T, transmitter Transmitter, ops ...OCRTransmitterOption) *dualContractTransmitter {
contractABI, err := abi.JSON(strings.NewReader(ocr2aggregator.OCR2AggregatorMetaData.ABI))
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/txm/clientwrappers/dualbroadcast/meta_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,6 @@ func (a *MetaClient) SendOperation(ctx context.Context, tx *types.Transaction, a
return fmt.Errorf("failed to update signed attempt for txID: %v, err: %w", tx.ID, err)
}
a.lggr.Infow("Intercepted attempt for tx", "txID", tx.ID, "hash", signedTx.Hash(), "toAddress", meta.ToAddress, "gasLimit", meta.GasLimit,
"TipCap", tip, "FeeCap", meta.MaxFeePerGas)
"TipCap", tip, "FeeCap", meta.MaxFeePerGas, "transactionLifecycleID", tx.GetTransactionLifecycleID(a.lggr))
return a.c.SendTransaction(ctx, signedTx)
}
10 changes: 7 additions & 3 deletions pkg/txm/txm.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (t *Txm) HealthReport() map[string]error {
func (t *Txm) CreateTransaction(ctx context.Context, txRequest *types.TxRequest) (tx *types.Transaction, err error) {
tx, err = t.txStore.CreateTransaction(ctx, txRequest)
if err == nil {
t.lggr.Infow("Created transaction", "tx", tx)
t.lggr.Infow("Created transaction", "txID", tx.ID, "tx", tx, "transactionLifecycleID", tx.GetTransactionLifecycleID(t.lggr))
}
return
}
Expand All @@ -188,7 +188,10 @@ func (t *Txm) Trigger(address common.Address) {
if !exists {
return
}
triggerCh <- struct{}{}
select {
case triggerCh <- struct{}{}:
default:
}
Comment on lines +191 to +194
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.

Can't fully oversee the consequences of this change, can you explain this a bit?

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.

Yes, this is the default way in go to not block the caller and just fill the channel and exit. Before we would (mistakenly) block after the buffer was filled. The old way may impact throughput on some rare cases, hence the change.

}) {
t.lggr.Error("Txm unstarted")
}
Expand Down Expand Up @@ -333,7 +336,7 @@ func (t *Txm) sendTransactionWithError(ctx context.Context, tx *types.Transactio
}
start := time.Now()
txErr := t.client.SendTransaction(ctx, tx, attempt)
t.lggr.Infow("Broadcasted attempt", "tx", tx, "attempt", attempt, "duration", time.Since(start), "txErr: ", txErr)
t.lggr.Infow("Broadcasted attempt", "tx", tx, "attempt", attempt, "transactionLifecycleID", tx.GetTransactionLifecycleID(t.lggr), "duration", time.Since(start), "txErr", txErr)
if txErr != nil && t.errorHandler != nil {
if err = t.errorHandler.HandleError(ctx, tx, txErr, t.txStore, t.SetNonce, false); err != nil {
return
Expand Down Expand Up @@ -440,6 +443,7 @@ func (t *Txm) extractMetrics(ctx context.Context, txs []*types.Transaction) []ui
if tx.InitialBroadcastAt != nil {
t.Metrics.RecordTimeUntilTxConfirmed(ctx, float64(time.Since(*tx.InitialBroadcastAt)))
}
t.lggr.Infow("Confirmed transaction", "txID", tx.ID, "transactionLifecycleID", tx.GetTransactionLifecycleID(t.lggr))
}
return confirmedTxIDs
}
16 changes: 16 additions & 0 deletions pkg/txm/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/sqlutil"
clnull "github.com/smartcontractkit/chainlink-common/pkg/utils/null"

Expand Down Expand Up @@ -106,6 +107,18 @@ func (t *Transaction) GetMeta() (*TxMeta, error) {
return &m, nil
}

func (t *Transaction) GetTransactionLifecycleID(lggr logger.SugaredLogger) string {
meta, err := t.GetMeta()
if err != nil {
lggr.Errorw("failed to get meta of the transaction", "err", err)
return ""
}
if meta != nil && meta.TransactionLifecycleID != nil {
return *meta.TransactionLifecycleID
}
return ""
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.

Maybe unknown is clearer? It will differentiate it from the error case

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.

I don't think unknown is consistent. I'd rather have it empty, or probably not log the field at all if it's nil in a future PR.

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.

Fair, I do think it would be nice to differentiate between the unknown/unset situation and the error situation though

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.

Sure, but we shouldn't relay an error in an unrelated log field just because the underlying method happened to fail. That was the intention of the error log above. Each time you'll make the call you'll see the empty field but you'll also see the failed to get meta of the transaction message. If you feel there is a better way to handle this, happy to discuss.

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.

I'm not completely following this. Let's say we grep the logs for broadcasted transactions by querying e.g. <logs> |= "Broadcasted attempt". We will then see a number of broadcasts with their tracing ID attached. Then we can do a new search with <logs> | tracingID="<tracingID>".

However, if tracing ID is empty, then we don't know what happened: is it actually not set or were we unable to retrieve Meta? We won't see the failed to get meta of the transaction log line because we've already scoped the query to Broadcasted attempt.

Anyway, I'm fine leaving it like this since there's a very low chance GetMeta() returns an error, this would only happen on malformed json.

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.

Yes, if you filter specifically with <logs> |= "Broadcasted attempt" you won't see it, but if you filter for the TXM logs, which is usually what we do, you'll be able to see a bunch of errors explaining the issue. If you're expecting a tracingID value and it's not there it's somewhat easy to infer what happened. Overall, I see the point, it's just I don't want to pollute the implementation with a custom handling for such an edge case, while we can retrieve the information by another log

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.

Also this might be eventually split up to individual fields i.e. configDigest, epoch, round

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.

That's true. Ok let's leave it like this

}

type Attempt struct {
ID uint64
TxID uint64
Expand Down Expand Up @@ -184,6 +197,9 @@ type TxMeta struct {
// Dual Broadcast
DualBroadcast *bool `json:"DualBroadcast,omitempty"`
DualBroadcastParams *string `json:"DualBroadcastParams,omitempty"`

// TransactionLifecycleID is used for tracing the entire lifecycle of a transaction from OCR Transmit to confirmation on-chain.
TransactionLifecycleID *string `json:"TransactionLifecycleID,omitempty"`
}

type QueueingTxStrategy struct {
Expand Down
Loading