Skip to content

Commit 72017e8

Browse files
authored
Merge branch 'main' into CRE-361-configtest-2
2 parents ea43e20 + 4aeae90 commit 72017e8

File tree

3 files changed

+400
-69
lines changed

3 files changed

+400
-69
lines changed

pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go

Lines changed: 241 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,68 @@
11
package datafeeds
22

33
import (
4+
"crypto/sha256"
45
"encoding/binary"
56
"encoding/json"
67
"errors"
78
"fmt"
89
"math/big"
910
"strconv"
1011

12+
chainselectors "github.com/smartcontractkit/chain-selectors"
1113
ocrcommon "github.com/smartcontractkit/libocr/commontypes"
1214
ocr2types "github.com/smartcontractkit/libocr/offchainreporting2/types"
1315
ocr3types "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types"
1416

1517
"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
1618
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types"
1719
"github.com/smartcontractkit/chainlink-common/pkg/logger"
20+
"github.com/smartcontractkit/chainlink-common/pkg/types/chains/solana"
1821
"github.com/smartcontractkit/chainlink-protos/cre/go/values"
1922
)
2023

2124
var (
2225
ErrNoMatchingChainSelector = errors.New("no matching chain selector found")
2326
)
2427

28+
type SolanaEncoderKey = string
29+
30+
const (
31+
/*
32+
OutputFormat for solana:
33+
"account_context_hash": <"hash">,
34+
"payload": []reports{timestamp uint32, answer *big.Int, dataId [16]byte }
35+
Solana encoder compatible idl config:
36+
encoderConfig := map[string]any{
37+
report_schema": `{
38+
"kind": "struct",
39+
"fields": [
40+
{ "name": "payload", "type": { "vec": { "defined": "DecimalReport" } } }
41+
]
42+
}`,
43+
"defined_types": `[
44+
{
45+
"name":"DecimalReport",
46+
"type":{
47+
"kind":"struct",
48+
"fields":[
49+
{ "name":"timestamp", "type":"u32" },
50+
{ "name":"answer", "type":"u128" },
51+
{ "name": "dataId", "type": {"array": ["u8",16]}}
52+
]
53+
}
54+
}
55+
]`,
56+
}
57+
58+
*/
59+
TopLevelPayloadListFieldName = SolanaEncoderKey("payload")
60+
TopLevelAccountCtxHashFieldName = SolanaEncoderKey("account_context_hash")
61+
SolTimestampOutputFieldName = SolanaEncoderKey("timestamp")
62+
SolAnswerOutputFieldName = SolanaEncoderKey("answer")
63+
SolDataIDOutputFieldName = SolanaEncoderKey("dataId")
64+
)
65+
2566
// secureMintReport represents the inner report structure, mimics the Report type in the SM plugin repo
2667
type secureMintReport struct {
2768
ConfigDigest ocr2types.ConfigDigest `json:"configDigest"`
@@ -33,11 +74,17 @@ type secureMintReport struct {
3374
// chainSelector represents the chain selector type, mimics the ChainSelector type in the SM plugin repo
3475
type chainSelector uint64
3576

77+
type SolanaConfig struct {
78+
// Add Solana-specific configuration fields here
79+
AccountContext solana.AccountMetaSlice `mapstructure:"remaining_accounts"`
80+
}
81+
3682
// SecureMintAggregatorConfig is the config for the SecureMint aggregator.
3783
// This aggregator is designed to pick out reports for a specific chain selector.
3884
type SecureMintAggregatorConfig struct {
3985
// TargetChainSelector is the chain selector to look for
4086
TargetChainSelector chainSelector `mapstructure:"targetChainSelector"`
87+
Solana SolanaConfig `mapstructure:"solana"`
4188
}
4289

4390
// ToMap converts the SecureMintAggregatorConfig to a values.Map, which is suitable for the
@@ -54,7 +101,152 @@ func (c SecureMintAggregatorConfig) ToMap() (*values.Map, error) {
54101
var _ types.Aggregator = (*SecureMintAggregator)(nil)
55102

56103
type SecureMintAggregator struct {
57-
config SecureMintAggregatorConfig
104+
config SecureMintAggregatorConfig
105+
registry FormatterFactory
106+
}
107+
108+
type ChainReportFormatter interface {
109+
PackReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error)
110+
}
111+
112+
type EVMReportFormatter struct {
113+
TargetChainSelector uint64
114+
}
115+
116+
func (f *EVMReportFormatter) PackReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) {
117+
// Convert chain selector to bytes for data ID
118+
// Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s
119+
var chainSelectorAsDataID [16]byte
120+
chainSelectorAsDataID[0] = 0x04
121+
binary.BigEndian.PutUint64(chainSelectorAsDataID[1:], uint64(f.TargetChainSelector))
122+
123+
smReportAsAnswer, err := packSecureMintReportIntoUint224ForEVM(report.Mintable, report.Block)
124+
if err != nil {
125+
return nil, fmt.Errorf("failed to pack secure mint report for evm into uint224: %w", err)
126+
}
127+
128+
lggr.Debugw("packed report into answer", "smReportAsAnswer", smReportAsAnswer)
129+
130+
// This is what the DF Cache contract expects:
131+
// abi: "(bytes16 dataId, uint32 timestamp, uint224 answer)[] Reports"
132+
toWrap := []any{
133+
map[EVMEncoderKey]any{
134+
DataIDOutputFieldName: chainSelectorAsDataID,
135+
AnswerOutputFieldName: smReportAsAnswer,
136+
TimestampOutputFieldName: int64(report.Block),
137+
},
138+
}
139+
140+
wrappedReport, err := values.NewMap(map[string]any{
141+
TopLevelListOutputFieldName: toWrap,
142+
})
143+
if err != nil {
144+
return nil, fmt.Errorf("failed to wrap report: %w", err)
145+
}
146+
147+
return wrappedReport, nil
148+
}
149+
150+
func NewEVMReportFormatter(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) {
151+
return &EVMReportFormatter{TargetChainSelector: chainSelector}, nil
152+
}
153+
154+
type SolanaReportFormatter struct {
155+
TargetChainSelector uint64
156+
OnReportAccounts solana.AccountMetaSlice
157+
}
158+
159+
func (f *SolanaReportFormatter) PackReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) {
160+
// TEMPORARY DATA ID
161+
// Convert chain selector to bytes for data ID
162+
// Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s
163+
var chainSelectorAsDataID [16]byte
164+
chainSelectorAsDataID[0] = 0x04
165+
binary.BigEndian.PutUint64(chainSelectorAsDataID[1:], uint64(f.TargetChainSelector))
166+
167+
// pack answer
168+
smReportAsAnswer, err := packSecureMintReportIntoU128ForSolana(report.Mintable, report.Block)
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to pack secure mint report for solana into u128: %w", err)
171+
}
172+
lggr.Debugw("packed report into answer", "smReportAsAnswer", smReportAsAnswer)
173+
174+
// hash account contexts
175+
var accounts = make([]byte, 0)
176+
for _, acc := range f.OnReportAccounts {
177+
accounts = append(accounts, acc.PublicKey[:]...)
178+
}
179+
accountContextHash := sha256.Sum256(accounts)
180+
lggr.Debugw("calculated account context hash", "accountContextHash", accountContextHash)
181+
182+
if report.Block > (1<<32 - 1) { // timestamp must fit in u32 in solana
183+
return nil, fmt.Errorf("timestamp exceeds u32 bounds: %v", report.Block)
184+
}
185+
186+
toWrap := []any{
187+
map[SolanaEncoderKey]any{
188+
SolTimestampOutputFieldName: uint32(report.Block), // TODO: Verify with Michael/Geert timestamp should be block number?
189+
SolAnswerOutputFieldName: smReportAsAnswer,
190+
SolDataIDOutputFieldName: chainSelectorAsDataID,
191+
},
192+
}
193+
194+
wrappedReport, err := values.NewMap(map[string]any{
195+
TopLevelAccountCtxHashFieldName: accountContextHash,
196+
TopLevelPayloadListFieldName: toWrap,
197+
})
198+
199+
if err != nil {
200+
return nil, fmt.Errorf("failed to wrap report: %w", err)
201+
}
202+
203+
return wrappedReport, nil
204+
}
205+
206+
func NewSolanaReportFormatter(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) {
207+
return &SolanaReportFormatter{TargetChainSelector: chainSelector, OnReportAccounts: config.Solana.AccountContext}, nil
208+
}
209+
210+
type Builder func(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error)
211+
212+
type FormatterFactory interface {
213+
Register(chainSelector uint64, builder Builder)
214+
Get(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error)
215+
}
216+
217+
type DefaultFormatterFactory struct {
218+
builders map[uint64]Builder
219+
}
220+
221+
func (r *DefaultFormatterFactory) Register(chainSelector uint64, builder Builder) {
222+
r.builders[chainSelector] = builder
223+
}
224+
225+
func (r *DefaultFormatterFactory) Get(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) {
226+
b, ok := r.builders[chainSelector]
227+
if !ok {
228+
return nil, fmt.Errorf("no formatter registered for chain selector: %d", chainSelector)
229+
}
230+
231+
return b(chainSelector, config)
232+
}
233+
234+
func NewDefaultFormatterFactory() FormatterFactory {
235+
r := DefaultFormatterFactory{
236+
builders: map[uint64]Builder{},
237+
}
238+
239+
// EVM
240+
for _, selector := range chainselectors.EvmChainIdToChainSelector() {
241+
r.Register(selector, NewEVMReportFormatter)
242+
}
243+
244+
// Solana
245+
for _, selector := range chainselectors.SolanaChainIdToChainSelector() {
246+
r.Register(selector, NewSolanaReportFormatter)
247+
}
248+
249+
return &r
58250
}
59251

60252
// NewSecureMintAggregator creates a new SecureMintAggregator instance based on the provided configuration.
@@ -64,8 +256,11 @@ func NewSecureMintAggregator(config values.Map) (types.Aggregator, error) {
64256
if err != nil {
65257
return nil, fmt.Errorf("failed to parse config (%+v): %w", config, err)
66258
}
259+
registry := NewDefaultFormatterFactory()
260+
67261
return &SecureMintAggregator{
68-
config: parsedConfig,
262+
config: parsedConfig,
263+
registry: registry,
69264
}, nil
70265
}
71266

@@ -172,33 +367,19 @@ func (a *SecureMintAggregator) createOutcome(lggr logger.Logger, report *secureM
172367
lggr = logger.Named(lggr, "SecureMintAggregator")
173368
lggr.Debugw("createOutcome called", "report", report)
174369

175-
// Convert chain selector to bytes for data ID
176-
// Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s
177-
var chainSelectorAsDataID [16]byte
178-
chainSelectorAsDataID[0] = 0x04
179-
binary.BigEndian.PutUint64(chainSelectorAsDataID[1:], uint64(a.config.TargetChainSelector))
370+
reportFormatter, err := a.registry.Get(
371+
uint64(a.config.TargetChainSelector),
372+
a.config,
373+
)
180374

181-
smReportAsAnswer, err := packSecureMintReportIntoUint224ForEVM(report.Mintable, report.Block)
182375
if err != nil {
183-
return nil, fmt.Errorf("failed to pack secure mint report for evm into uint224: %w", err)
376+
return nil, fmt.Errorf("encountered issue fetching report formatter in createOutcome %w", err)
184377
}
185-
lggr.Debugw("packed report into answer", "smReportAsAnswer", smReportAsAnswer)
186378

187-
// This is what the DF Cache contract expects:
188-
// abi: "(bytes16 dataId, uint32 timestamp, uint224 answer)[] Reports"
189-
toWrap := []any{
190-
map[EVMEncoderKey]any{
191-
DataIDOutputFieldName: chainSelectorAsDataID,
192-
AnswerOutputFieldName: smReportAsAnswer,
193-
TimestampOutputFieldName: int64(report.Block),
194-
},
195-
}
379+
wrappedReport, err := reportFormatter.PackReport(lggr, report)
196380

197-
wrappedReport, err := values.NewMap(map[string]any{
198-
TopLevelListOutputFieldName: toWrap,
199-
})
200381
if err != nil {
201-
return nil, fmt.Errorf("failed to wrap report: %w", err)
382+
return nil, fmt.Errorf("encountered issue generating report in createOutcome %w", err)
202383
}
203384

204385
reportsProto := values.Proto(wrappedReport)
@@ -220,7 +401,8 @@ func (a *SecureMintAggregator) createOutcome(lggr logger.Logger, report *secureM
220401
// parseSecureMintConfig parses the user-facing, type-less, SecureMint aggregator config into the internal typed config.
221402
func parseSecureMintConfig(config values.Map) (SecureMintAggregatorConfig, error) {
222403
type rawConfig struct {
223-
TargetChainSelector string `mapstructure:"targetChainSelector"`
404+
TargetChainSelector string `mapstructure:"targetChainSelector"`
405+
Solana SolanaConfig `mapstructure:"solana"`
224406
}
225407

226408
var rawCfg rawConfig
@@ -239,12 +421,13 @@ func parseSecureMintConfig(config values.Map) (SecureMintAggregatorConfig, error
239421

240422
parsedConfig := SecureMintAggregatorConfig{
241423
TargetChainSelector: chainSelector(sel),
424+
Solana: rawCfg.Solana,
242425
}
243426

244427
return parsedConfig, nil
245428
}
246429

247-
var maxMintable = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)) // 2^128 - 1
430+
var maxMintableEVM = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1)) // 2^128 - 1
248431

249432
// packSecureMintReportIntoUint224ForEVM packs the mintable and block number into a single uint224 so that it can be used as a price in the DF Cache contract
250433
// (top 32 - not used / middle 64 - block number / lower 128 - mintable amount)
@@ -255,8 +438,8 @@ func packSecureMintReportIntoUint224ForEVM(mintable *big.Int, blockNumber uint64
255438
}
256439

257440
// Validate that mintable fits in 128 bits
258-
if mintable.Cmp(maxMintable) > 0 {
259-
return nil, fmt.Errorf("mintable amount %v exceeds maximum 128-bit value %v", mintable, maxMintable)
441+
if mintable.Cmp(maxMintableEVM) > 0 {
442+
return nil, fmt.Errorf("mintable amount %v exceeds maximum 128-bit value %v", mintable, maxMintableEVM)
260443
}
261444

262445
packed := big.NewInt(0)
@@ -269,3 +452,34 @@ func packSecureMintReportIntoUint224ForEVM(mintable *big.Int, blockNumber uint64
269452

270453
return packed, nil
271454
}
455+
456+
var maxMintableSolana = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 91), big.NewInt(1)) // 2^91 - 1
457+
var maxBlockNumberSolana uint64 = 1<<36 - 1 // 2^36 - 1
458+
459+
// TODO: will ripcord be added for top bit?
460+
// (top 1 - not used / middle 36 - block number / lower 91 - mintable amount)
461+
func packSecureMintReportIntoU128ForSolana(mintable *big.Int, blockNumber uint64) (*big.Int, error) {
462+
// Handle nil mintable
463+
if mintable == nil {
464+
return nil, fmt.Errorf("mintable cannot be nil")
465+
}
466+
467+
// Validate that mintable fits in 91 bits
468+
if mintable.Cmp(maxMintableSolana) > 0 {
469+
return nil, fmt.Errorf("mintable amount %v exceeds maximum 91-bit value %v", mintable, maxMintableSolana)
470+
}
471+
472+
packed := big.NewInt(0)
473+
// Put mintable in lower 91 bits
474+
packed.Or(packed, mintable)
475+
476+
if blockNumber > maxBlockNumberSolana {
477+
return nil, fmt.Errorf("block number %d exceeds maximum 36-bit value %d", blockNumber, maxBlockNumberSolana)
478+
}
479+
480+
// Put block number in middle 36 bits (bits 91-126)
481+
blockNumberAsBigInt := new(big.Int).SetUint64(blockNumber)
482+
packed.Or(packed, new(big.Int).Lsh(blockNumberAsBigInt, 91))
483+
484+
return packed, nil
485+
}

0 commit comments

Comments
 (0)