Skip to content

Commit 5f16c13

Browse files
[wip] feat: implement SolanaWorker.execute
1 parent 89fd5b3 commit 5f16c13

8 files changed

Lines changed: 235 additions & 19 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/prometheus/client_golang v1.21.1
1515
github.com/samber/lo v1.47.0
1616
github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240917103524-56f1a8d2cd4b
17-
github.com/smartcontractkit/chain-selectors v1.0.48
17+
github.com/smartcontractkit/chain-selectors v1.0.55
1818
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250527111814-1c5342edf3b4
1919
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.7
2020
github.com/smartcontractkit/mcms v0.20.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
428428
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
429429
github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240917103524-56f1a8d2cd4b h1:ejMDDk74A9cTfeDow+QArm8Z+bZ9QwGP/1DPCiwPqWM=
430430
github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240917103524-56f1a8d2cd4b/go.mod h1:p7L/xNEQpHDdZtgFA6/FavuZHqvV3kYhQysxBywmq1k=
431-
github.com/smartcontractkit/chain-selectors v1.0.48 h1:wa03tlcGj08qZfv1p+LXZIimpTBBH2CzTxHVqXGrfec=
432-
github.com/smartcontractkit/chain-selectors v1.0.48/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8=
431+
github.com/smartcontractkit/chain-selectors v1.0.55 h1:jn8cwTBEzAi/eQRDO7EbFpZFn60yxygnSSPP2HPbYs0=
432+
github.com/smartcontractkit/chain-selectors v1.0.55/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8=
433433
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250527111814-1c5342edf3b4 h1:i8/YmASm4mR0R2VSeM6p6reoMO+fnR/w+LLAXLQVDc4=
434434
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250527111814-1c5342edf3b4/go.mod h1:k3/Z6AvwurPUlfuDFEonRbkkiTSgNSrtVNhJEWNlUZA=
435435
github.com/smartcontractkit/chainlink-common v0.6.1-0.20250329081313-84ec641e0758 h1:SyaVoJtYZ54dO4HyUcfWsuvC0hRsjk+Lyy/k++WQc7Y=

pkg/timelock/operations_solana.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package timelock
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/gagliardetto/solana-go"
9+
"github.com/gagliardetto/solana-go/rpc"
10+
"github.com/samber/lo"
11+
mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana"
12+
mcmstypes "github.com/smartcontractkit/mcms/types"
13+
14+
timelockbindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock"
15+
solanautils "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common"
16+
)
17+
18+
// execute runs the CallScheduled operation if:
19+
// - The predecessor operation is finished
20+
// - The operation is ready to be executed
21+
// Otherwise the operation will throw an info log and wait for a future tick.
22+
func (tw *WorkerSolana) execute(ctx context.Context, op []TimelockCallScheduled) {
23+
if len(op) == 0 {
24+
tw.logger.Warn("no calls given")
25+
return
26+
}
27+
opId := op[0].Id()
28+
29+
isReady, err := tw.inspector.IsOperationReady(ctx, tw.timelockAddress, opId)
30+
if err != nil {
31+
tw.logger.Errorf("unable to read operation %x \"ready\" status: %s", opId, err.Error())
32+
return
33+
}
34+
if !isReady {
35+
tw.logger.Infof("skipping operation %x: not ready", opId)
36+
return
37+
}
38+
39+
mcmsTransactions, err := tw.mapCallScheduledEventsToMcmsTransactions(ctx, op)
40+
if err != nil {
41+
tw.logger.Errorf("unable to convert call scheduled events to mcms transactions: %v", err)
42+
return
43+
}
44+
batchOp := mcmstypes.BatchOperation{Transactions: mcmsTransactions}
45+
46+
tw.logger.Debugf("execute operation %x", opId)
47+
timelockExecutor := mcmssolanasdk.NewTimelockExecutor(tw.solanaClient, tw.privateKey)
48+
result, err := timelockExecutor.Execute(ctx, batchOp, tw.timelockAddress, op[0].Predecessor(), op[0].Salt())
49+
if err != nil {
50+
tw.logger.Errorf("execute operation %x error: %s", opId, err.Error())
51+
return
52+
}
53+
54+
tw.logger.Infof("execute operation %x success: %s", opId, result.Hash)
55+
}
56+
57+
func (tw *WorkerSolana) mapCallScheduledEventsToMcmsTransactions(
58+
ctx context.Context, events []TimelockCallScheduled,
59+
) ([]mcmstypes.Transaction, error) {
60+
transactions := make([]mcmstypes.Transaction, len(events))
61+
for i, event := range events {
62+
solanaEvent := event.(*solanaTimelockCallScheduled).callScheduledEvent
63+
64+
accounts, err := tw.getAccountsFromOperation(ctx, solanaEvent.ID)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to get accounts from operation pda: %w", err)
67+
}
68+
69+
additionalFields := mcmssolanasdk.AdditionalFields{
70+
Accounts: accounts,
71+
Value: nil, // REVIEW
72+
}
73+
additionalFieldsJson, err := json.Marshal(additionalFields)
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to marshsal additional fields: %w", err)
76+
}
77+
78+
transactions[i] = mcmstypes.Transaction{
79+
To: solanaEvent.Target.String(),
80+
Data: solanaEvent.Data,
81+
AdditionalFields: additionalFieldsJson,
82+
}
83+
}
84+
return transactions, nil
85+
}
86+
87+
func (tw *WorkerSolana) getAccountsFromOperation(ctx context.Context, operationID operationKey) ([]*solana.AccountMeta, error) {
88+
_, pdaSeed, err := mcmssolanasdk.ParseContractAddress(tw.timelockAddress)
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to parse timelock address: %w", err)
91+
}
92+
93+
operationPDA, err := FindOperationPDA(tw.timelockProgramKey, pdaSeed, operationID)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to find operation PDA: %w", err)
96+
}
97+
98+
var scheduledOperation timelockbindings.Operation
99+
err = solanautils.GetAccountDataBorshInto(ctx, tw.solanaClient, operationPDA, rpc.CommitmentConfirmed, &scheduledOperation)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to get operation data from PDA account: %w", err)
102+
}
103+
tw.logger.Debugf("scheduledOpAccount: %+v", scheduledOperation)
104+
105+
accountMap := make(map[string]*solana.AccountMeta)
106+
for _, instruction := range scheduledOperation.Instructions {
107+
// add program ID of the instruction as an account(CPI)
108+
progKey := instruction.ProgramId.String()
109+
_, exists := accountMap[progKey]
110+
if !exists {
111+
accountMap[progKey] = &solana.AccountMeta{
112+
PublicKey: instruction.ProgramId,
113+
IsSigner: false,
114+
IsWritable: false, // program accounts are never writable
115+
}
116+
}
117+
118+
// all other accounts from the instruction
119+
for _, account := range instruction.Accounts {
120+
key := account.Pubkey.String()
121+
existingAccount, exists := accountMap[key]
122+
if exists {
123+
existingAccount.IsWritable = existingAccount.IsWritable || account.IsWritable
124+
accountMap[key] = existingAccount
125+
} else {
126+
accountMap[key] = &solana.AccountMeta{
127+
PublicKey: account.Pubkey,
128+
IsSigner: false, // force false for CPI
129+
IsWritable: account.IsWritable,
130+
}
131+
}
132+
}
133+
}
134+
135+
return lo.Values(accountMap), nil
136+
137+
// // todo: debugging, remove after PoC
138+
// t.Log(len(remainingAccounts), "remaining accounts")
139+
// t.Log(len(op1.RemainingAccounts()), "original remaining accounts")
140+
//
141+
// t.Log("--- NEW IMPLEMENTATION ACCOUNTS ---")
142+
// for i, acc := range remainingAccounts {
143+
// t.Logf("Account %d: PubKey=%s, IsSigner=%t, IsWritable=%t",
144+
// i, acc.PublicKey.String(), acc.IsSigner, acc.IsWritable)
145+
// }
146+
//
147+
// t.Log("--- ORIGINAL IMPLEMENTATION ACCOUNTS ---")
148+
// for i, acc := range op1.RemainingAccounts() {
149+
// t.Logf("Account %d: PubKey=%s, IsSigner=%t, IsWritable=%t",
150+
// i, acc.PublicKey.String(), acc.IsSigner, acc.IsWritable)
151+
// }
152+
//
153+
// t.Log("--- CHECKING FOR MISSING ACCOUNTS ---")
154+
// originalMap := make(map[string]bool)
155+
// for _, acc := range op1.RemainingAccounts() {
156+
// originalMap[acc.PublicKey.String()] = true
157+
// }
158+
//
159+
// for _, acc := range remainingAccounts {
160+
// if _, exists := originalMap[acc.PublicKey.String()]; !exists {
161+
// t.Logf("WARNING: Account in new implementation NOT found in original: %s",
162+
// acc.PublicKey.String())
163+
// }
164+
// }
165+
166+
// // Check for extra accounts
167+
// newMap := make(map[string]bool)
168+
// for _, acc := range remainingAccounts {
169+
// newMap[acc.PublicKey.String()] = true
170+
// }
171+
//
172+
// for _, acc := range op1.RemainingAccounts() {
173+
// if _, exists := newMap[acc.PublicKey.String()]; !exists {
174+
// t.Logf("WARNING: Account in original implementation NOT found in new: %s",
175+
// acc.PublicKey.String())
176+
// }
177+
// }
178+
}
179+
180+
func FindOperationPDA(
181+
programID solana.PublicKey, timelockID mcmssolanasdk.PDASeed, opID [32]byte,
182+
) (solana.PublicKey, error) {
183+
seeds := [][]byte{[]byte("timelock_operation"), timelockID[:], opID[:]}
184+
return findPDA(programID, seeds)
185+
}
186+
187+
func findPDA(programID solana.PublicKey, seeds [][]byte) (solana.PublicKey, error) {
188+
pda, _, err := solana.FindProgramAddress(seeds, programID)
189+
if err != nil {
190+
return solana.PublicKey{}, fmt.Errorf("unable to find %s pda: %w", string(seeds[0]), err)
191+
}
192+
193+
return pda, nil
194+
}

pkg/timelock/scheduler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ type TimelockCallScheduled interface {
2626
Index() int
2727
BlockNumber() *big.Int
2828
TxHash() string
29+
Predecessor() eth.Hash
30+
Salt() eth.Hash
2931
}
3032

3133
type Scheduler interface {

pkg/timelock/scheduler_evm.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"math/big"
66

7+
eth "github.com/ethereum/go-ethereum/common"
78
bindings "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers"
89
)
910

@@ -34,3 +35,11 @@ func (cs *evmTimelockCallScheduled) BlockNumber() *big.Int {
3435
func (cs *evmTimelockCallScheduled) TxHash() string {
3536
return fmt.Sprintf("%x", cs.callScheduled.Raw.TxHash[:])
3637
}
38+
39+
func (cs *evmTimelockCallScheduled) Predecessor() eth.Hash {
40+
return cs.callScheduled.Predecessor
41+
}
42+
43+
func (cs *evmTimelockCallScheduled) Salt() eth.Hash {
44+
return cs.callScheduled.Salt
45+
}

pkg/timelock/scheduler_solana.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package timelock
22

33
import (
44
"math/big"
5+
6+
eth "github.com/ethereum/go-ethereum/common"
57
)
68

79
var _ TimelockCallScheduled = NewEVMTimelockCallScheduled(nil)
@@ -31,3 +33,11 @@ func (cs *solanaTimelockCallScheduled) BlockNumber() *big.Int {
3133
func (cs *solanaTimelockCallScheduled) TxHash() string {
3234
return cs.callScheduledEvent.TxHash
3335
}
36+
37+
func (cs *solanaTimelockCallScheduled) Predecessor() eth.Hash {
38+
return cs.callScheduledEvent.Predecessor
39+
}
40+
41+
func (cs *solanaTimelockCallScheduled) Salt() eth.Hash {
42+
return cs.callScheduledEvent.Salt
43+
}

pkg/timelock/solana_event_types.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"math/big"
99
"strings"
1010

11+
eth "github.com/ethereum/go-ethereum/common"
1112
bin "github.com/gagliardetto/binary"
1213
"github.com/gagliardetto/solana-go"
1314
"github.com/gagliardetto/solana-go/rpc"
@@ -34,8 +35,8 @@ type SolanaTimelockCallScheduledEvent struct {
3435
ID operationKey
3536
Index uint64
3637
Target solana.PublicKey
37-
Predecessor operationKey
38-
Salt [32]byte
38+
Predecessor eth.Hash
39+
Salt eth.Hash
3940
Delay uint64
4041
Data []byte
4142
BlockNumber *big.Int `borsh_skip:"true"`

pkg/timelock/worker_solana.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import (
1212
"github.com/gagliardetto/solana-go"
1313
"github.com/gagliardetto/solana-go/rpc"
1414
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
15-
16-
"github.com/smartcontractkit/mcms/sdk"
17-
solanasdk "github.com/smartcontractkit/mcms/sdk/solana"
1815
"go.uber.org/zap"
1916

17+
mcmssdk "github.com/smartcontractkit/mcms/sdk"
18+
mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana"
19+
2020
"github.com/smartcontractkit/timelock-worker/pkg/isclosed"
2121
)
2222

@@ -25,13 +25,13 @@ import (
2525
type WorkerSolana struct {
2626
solanaClient *rpc.Client
2727
timelockProgramKey solana.PublicKey
28-
instanceSeed solanasdk.PDASeed // instanceSeed is the seed used to derive the timelock instance.
29-
timelockFullAddress string // timelockFullAddress is the full address of the timelock program <programID>.<instanceSeed>
28+
instanceSeed mcmssolanasdk.PDASeed // instanceSeed is the seed used to derive the timelock instance.
29+
timelockFullAddress string // timelockFullAddress is the full address of the timelock program <programID>.<instanceSeed>
3030
pollPeriod int64
3131
listenerPollPeriod int64
3232
pollSize int
3333
dryRun bool
34-
inspector sdk.TimelockInspector // inspector is used to query timelock state
34+
inspector mcmssdk.TimelockInspector // inspector is used to query timelock state
3535
logger *zap.SugaredLogger
3636
privateKey solana.PrivateKey
3737
lastSignature *solana.Signature // last signature processed
@@ -60,7 +60,7 @@ func NewTimelockWorkerSolana(
6060
return nil, fmt.Errorf("invalid node URL: %s (accepted schemes are: %v)", nodeURL, validNodeUrlSchemes)
6161
}
6262

63-
timelockPubKey, instanceSeed, err := solanasdk.ParseContractAddress(timelockAddress)
63+
timelockPubKey, instanceSeed, err := mcmssolanasdk.ParseContractAddress(timelockAddress)
6464
if err != nil {
6565
return nil, fmt.Errorf("timelock addresses provided is not valid: %s", timelockAddress)
6666
}
@@ -83,7 +83,7 @@ func NewTimelockWorkerSolana(
8383

8484
// All variables provided are correct, start allocating new structures.
8585
client := rpc.New(nodeURL)
86-
inspector := solanasdk.NewTimelockInspector(client)
86+
inspector := mcmssolanasdk.NewTimelockInspector(client)
8787

8888
tWorker := &WorkerSolana{
8989
solanaClient: client,
@@ -100,9 +100,9 @@ func NewTimelockWorkerSolana(
100100
}
101101

102102
if dryRun {
103-
tWorker.scheduler = nil // TODO: add solana nopScheduler implementation
103+
tWorker.scheduler = newNopScheduler(logger)
104104
} else {
105-
tWorker.scheduler = nil // TODO: add solana Scheduler implementation
105+
tWorker.scheduler = newScheduler(time.Duration(pollPeriod)*time.Second, logger, tWorker.execute)
106106
}
107107

108108
return tWorker, nil
@@ -117,7 +117,7 @@ func (w *WorkerSolana) Listen(ctx context.Context) error {
117117
w.startLog()
118118

119119
// Run the scheduler to add/del operations in a thread-safe way.
120-
//schedulingDone = w.scheduler.runScheduler(ctxwc)
120+
// schedulingDone = w.scheduler.runScheduler(ctxwc)
121121

122122
//// Retrieve logs asynchronously.
123123
pollDone, txCh := w.pollNewTransactions(ctxwc)
@@ -137,7 +137,7 @@ func (w *WorkerSolana) Listen(ctx context.Context) error {
137137
w.logger.Info("shutting down timelock-worker")
138138
w.logger.Info("dumping operation store")
139139
// TODO: re-add when scheduler is implemented
140-
//w.scheduler.dumpOperationStore(time.Now)
140+
// w.scheduler.dumpOperationStore(time.Now)
141141

142142
// Wait for all goroutines to finish.
143143
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
@@ -342,7 +342,7 @@ func (w *WorkerSolana) handleEventCancelled(_ context.Context, event SolanaTimel
342342
Infow("event received, cancelling operation", "event type", eventCancelled)
343343

344344
// TODO: add scheduler call once scheduler is implemented
345-
//w.scheduler.delFromScheduler(event.ID)
345+
// w.scheduler.delFromScheduler(event.ID)
346346
}
347347

348348
// handleEventExecuted checks if the operation is done and deletes it from the scheduler if it is.
@@ -358,7 +358,7 @@ func (w *WorkerSolana) handleEventExecuted(ctx context.Context, event SolanaTime
358358
if isDone {
359359
logger.Infow("event received, deleting operation from scheduler", "event type ", eventCallExecuted)
360360
// TODO: add scheduler call once scheduler is implemented
361-
//w.scheduler.delFromScheduler(event.ID)
361+
// w.scheduler.delFromScheduler(event.ID)
362362
} else {
363363
logger.Warn("operation not done; skipping deletion from scheduler")
364364
}

0 commit comments

Comments
 (0)