Skip to content

Commit fa9f4fe

Browse files
committed
feat: add idempotency key support to prevent duplicate CCV verification processing
Updates aggregator database schema, API validation, and storage layer to use idempotency_key for deduplication instead of verification_timestamp. Modifies verifier pipeline to generate and propagate idempotency keys through the CCVDataWithIdempotencyKey paired struct from source readers to storage operations.
1 parent 153acf8 commit fa9f4fe

22 files changed

+165
-150
lines changed

aggregator/migrations/postgres/00001_create_all_tables.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ CREATE TABLE IF NOT EXISTS commit_verification_records (
1616
signature_s BYTEA NOT NULL DEFAULT '',
1717
ccv_node_data BYTEA NOT NULL,
1818
verification_timestamp BIGINT NOT NULL,
19+
idempotency_key TEXT NOT NULL,
1920
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
20-
CONSTRAINT unique_verification UNIQUE (message_id, committee_id, signer_address, verification_timestamp)
21+
CONSTRAINT unique_verification UNIQUE (message_id, committee_id, signer_address, idempotency_key)
2122
);
2223

2324
CREATE TABLE IF NOT EXISTS commit_aggregated_reports (

aggregator/pkg/handlers/validations.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import (
1212
func validateWriteRequest(req *pb.WriteCommitCCVNodeDataRequest) error {
1313
err := validation.ValidateStruct(
1414
req,
15-
validation.Field(&req.CcvNodeData, validation.Required))
15+
validation.Field(&req.CcvNodeData, validation.Required),
16+
validation.Field(&req.IdempotencyKey, validation.Required, validation.Length(1, 255)))
1617
if err != nil {
1718
return err
1819
}

aggregator/pkg/handlers/write_commit_ccv_data.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func (h *WriteCommitCCVNodeDataHandler) Handle(ctx context.Context, req *pb.Writ
7171
MessageWithCCVNodeData: *req.GetCcvNodeData(),
7272
IdentifierSigner: signer,
7373
CommitteeID: signer.CommitteeID,
74+
IdempotencyKey: req.GetIdempotencyKey(),
7475
}
7576
err := h.storage.SaveCommitVerification(signerCtx, &record)
7677
if err != nil {

aggregator/pkg/model/commit_verification_record.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ func (c CommitVerificationRecordIdentifier) ToIdentifier() string {
2626
type CommitVerificationRecord struct {
2727
IdentifierSigner *IdentifierSigner
2828
pb.MessageWithCCVNodeData
29-
CommitteeID CommitteeID
29+
CommitteeID CommitteeID
30+
IdempotencyKey string
3031
}
3132

3233
// GetID retrieves the unique identifier for the commit verification record.

aggregator/pkg/storage/postgres/database_storage.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ func (d *DatabaseStorage) SaveCommitVerification(ctx context.Context, record *mo
142142

143143
stmt := `INSERT INTO commit_verification_records
144144
(message_id, committee_id, participant_id, signer_address, source_chain_selector, dest_chain_selector,
145-
onramp_address, offramp_address, signature_r, signature_s, ccv_node_data, verification_timestamp)
146-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
147-
ON CONFLICT (message_id, committee_id, signer_address, verification_timestamp)
145+
onramp_address, offramp_address, signature_r, signature_s, ccv_node_data, verification_timestamp, idempotency_key)
146+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
147+
ON CONFLICT (message_id, committee_id, signer_address, idempotency_key)
148148
DO NOTHING`
149149

150150
ccvNodeData, err := proto.Marshal(&record.MessageWithCCVNodeData)
@@ -174,6 +174,7 @@ func (d *DatabaseStorage) SaveCommitVerification(ctx context.Context, record *mo
174174
record.IdentifierSigner.SignatureS[:],
175175
ccvNodeData,
176176
record.Timestamp,
177+
record.IdempotencyKey,
177178
)
178179
if err != nil {
179180
return fmt.Errorf("failed to save commit verification record: %w", err)

aggregator/pkg/storage/postgres/database_storage_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/ethereum/go-ethereum/common"
1212
"github.com/ethereum/go-ethereum/crypto"
13+
"github.com/google/uuid"
1314
"github.com/jmoiron/sqlx"
1415
"github.com/stretchr/testify/require"
1516
"github.com/testcontainers/testcontainers-go"
@@ -181,7 +182,8 @@ func createTestCommitVerificationRecord(msgWithCCV *pb.MessageWithCCVNodeData, s
181182
SignatureS: s32,
182183
CommitteeID: committeeID,
183184
},
184-
CommitteeID: committeeID,
185+
CommitteeID: committeeID,
186+
IdempotencyKey: uuid.NewString(), // Generate unique idempotency key for each record
185187
}
186188
}
187189

aggregator/tests/commit_verification_api_test.go

Lines changed: 39 additions & 96 deletions
Large diffs are not rendered by default.

aggregator/tests/fixture.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/ethereum/go-ethereum/crypto"
10+
"github.com/google/uuid"
1011
"github.com/stretchr/testify/require"
1112

1213
"github.com/smartcontractkit/chainlink-ccv/aggregator/pkg/model"
@@ -179,3 +180,19 @@ func NewMessageWithCCVNodeData(t *testing.T, message *protocol.Message, sourceVe
179180
}
180181
return ccvNodeData
181182
}
183+
184+
// NewWriteCommitCCVNodeDataRequest creates a new WriteCommitCCVNodeDataRequest with a generated idempotency key.
185+
func NewWriteCommitCCVNodeDataRequest(ccvNodeData *pb.MessageWithCCVNodeData) *pb.WriteCommitCCVNodeDataRequest {
186+
return &pb.WriteCommitCCVNodeDataRequest{
187+
CcvNodeData: ccvNodeData,
188+
IdempotencyKey: uuid.New().String(),
189+
}
190+
}
191+
192+
// NewWriteCommitCCVNodeDataRequestWithKey creates a new WriteCommitCCVNodeDataRequest with a specific idempotency key.
193+
func NewWriteCommitCCVNodeDataRequestWithKey(ccvNodeData *pb.MessageWithCCVNodeData, idempotencyKey string) *pb.WriteCommitCCVNodeDataRequest {
194+
return &pb.WriteCommitCCVNodeDataRequest{
195+
CcvNodeData: ccvNodeData,
196+
IdempotencyKey: idempotencyKey,
197+
}
198+
}

build/devenv/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ require (
3232
require (
3333
github.com/gorilla/websocket v1.5.3
3434
github.com/smartcontractkit/chainlink-ccv v0.0.0-00010101000000-000000000000
35-
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/go v0.0.0-20251028171604-3c9144b4db13
35+
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/go v0.0.0-20251030133409-21ff07fff5e9
3636
github.com/smartcontractkit/chainlink-testing-framework/wasp v1.51.1
3737
google.golang.org/grpc v1.76.0
3838
)

build/devenv/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,8 +1024,8 @@ github.com/smartcontractkit/chainlink-deployments-framework v0.56.0 h1:VkzslEC/a
10241024
github.com/smartcontractkit/chainlink-deployments-framework v0.56.0/go.mod h1:ObH5HJ4yXzTmQLc6Af+ufrTVcQ+ocasHJ0YBZjw5ZCM=
10251025
github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250826201006-c81344a26fc3 h1:mJP6yJq2woOZchX0KvhLiKxDPaS0Vy4vTDFH4nnFkXs=
10261026
github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250826201006-c81344a26fc3/go.mod h1:3Lsp38qxen9PABVF+O5eocveQev+hyo9HLAgRodBD4Q=
1027-
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/go v0.0.0-20251028171604-3c9144b4db13 h1:kbOA8/t8Eq+T70d4jCbuPjDCQAulnaOBnymtm6eR1ts=
1028-
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/go v0.0.0-20251028171604-3c9144b4db13/go.mod h1:KJkb85Mfxr/2vjPvAWWpq0/QJMAP1Bts1wMWWhRn4/E=
1027+
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/go v0.0.0-20251030133409-21ff07fff5e9 h1:vyj6oNj3cifQzg7jI/UCC59h8q3yPUzxnq3/lwrBMtc=
1028+
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/go v0.0.0-20251030133409-21ff07fff5e9/go.mod h1:KJkb85Mfxr/2vjPvAWWpq0/QJMAP1Bts1wMWWhRn4/E=
10291029
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2 h1:1/KdO5AbUr3CmpLjMPuJXPo2wHMbfB8mldKLsg7D4M8=
10301030
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250911124514-5874cc6d62b2/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q=
10311031
github.com/smartcontractkit/chainlink-protos/job-distributor v0.13.1 h1:PWwLGimBt37eDzpbfZ9V/ZkW4oCjcwKjKiAwKlSfPc0=

0 commit comments

Comments
 (0)