Skip to content

Commit 17381e4

Browse files
fix(solana): call ClearSignatures in SetRoot on "account in use" error (#374)
This PR enhances the Solana `SetRoot` implementation so as to handle "Account already in use" errors, which we got several times when executing changesets in mainnet. In theory, these errors are happening because Solana's SetRoot does not execute atomically -- instead, it requires at least 4 instructions: 1. initialize signatures 2. append signatures (at least once) 3. finalize signatures 4. set root If the first instruction completes successfully but any of the others fails, it'll leave the signatures PDA in a initialized state but without the root properly set. This prevents retries at the CI level from working as the program will then fail, stating the the pda "account is already in use". The workaround in this PR is to identify this error and issue a "clear signatures" instruction -- which should reset the signatures pda -- and then retry the SetRoot instructions as usual. The PR also bumps the chainlink-ccip version so as to include a patch from Terry Tata to a "send transaction" helper that should help deal with frequent RPC errors found in mainnet. [DX-768](https://smartcontract-it.atlassian.net/browse/DX-768) [DX-768]: https://smartcontract-it.atlassian.net/browse/DX-768?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent b804353 commit 17381e4

File tree

5 files changed

+126
-5
lines changed

5 files changed

+126
-5
lines changed

.changeset/swift-cougars-tickle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smartcontractkit/mcms": patch
3+
---
4+
5+
fix: call ClearSignatures in Solana's SetRoot upon receiving an "account in use" error

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ require (
1717
github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52
1818
github.com/smartcontractkit/chain-selectors v1.0.48
1919
github.com/smartcontractkit/chainlink-aptos v0.0.0-20250414155853-651b4e583ee9
20-
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250422205932-c33527859fd6
20+
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250512155317-4615e5659ce4
2121
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.7
2222
github.com/spf13/cast v1.7.1
2323
github.com/stretchr/testify v1.10.0
@@ -189,6 +189,7 @@ require (
189189
github.com/shoenig/go-m1cpu v0.1.6 // indirect
190190
github.com/shopspring/decimal v1.4.0 // indirect
191191
github.com/sirupsen/logrus v1.9.3 // indirect
192+
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250320090719-315440f5b0a7 // indirect
192193
github.com/smartcontractkit/chainlink-common v0.6.1-0.20250329081313-84ec641e0758 // indirect
193194
github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect
194195
github.com/smartcontractkit/libocr v0.0.0-20250220133800-f3b940c4f298 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,12 @@ github.com/smartcontractkit/chain-selectors v1.0.48 h1:wa03tlcGj08qZfv1p+LXZIimp
599599
github.com/smartcontractkit/chain-selectors v1.0.48/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8=
600600
github.com/smartcontractkit/chainlink-aptos v0.0.0-20250414155853-651b4e583ee9 h1:lw8RZ8IR4UX1M7djAB3IuMtcAqFX4Z4bzQczClfb8bs=
601601
github.com/smartcontractkit/chainlink-aptos v0.0.0-20250414155853-651b4e583ee9/go.mod h1:Sq/ddMOYch6ZuAnW2k5u9V4+TlGKFzuHQnTM8JXEU+g=
602+
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250320090719-315440f5b0a7 h1:/VKrPJRo4y58whyBRhc9Fszu2eTNn0LNISaS0pjhTpk=
603+
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250320090719-315440f5b0a7/go.mod h1:AhqYIeGF2k94J+/gzRx5dQttlgUdZid2N6E4HlHVIVA=
602604
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250422205932-c33527859fd6 h1:EWpKT2h/jyqCcblfmtHuY5oN2k8k2Mrmi5KqX+hc2lo=
603605
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250422205932-c33527859fd6/go.mod h1:k3/Z6AvwurPUlfuDFEonRbkkiTSgNSrtVNhJEWNlUZA=
606+
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250512155317-4615e5659ce4 h1:ZopJiCUcshj79A3tjh0f5cncXEjlfp7QOPWFppb4gxM=
607+
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250512155317-4615e5659ce4/go.mod h1:k3/Z6AvwurPUlfuDFEonRbkkiTSgNSrtVNhJEWNlUZA=
604608
github.com/smartcontractkit/chainlink-common v0.6.1-0.20250329081313-84ec641e0758 h1:SyaVoJtYZ54dO4HyUcfWsuvC0hRsjk+Lyy/k++WQc7Y=
605609
github.com/smartcontractkit/chainlink-common v0.6.1-0.20250329081313-84ec641e0758/go.mod h1:ASXpANdCfcKd+LF3Vhz37q4rmJ/XYQKEQ3La1k7idp0=
606610
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.7 h1:E7k5Sym9WnMOc4X40lLnQb6BMosxi8DfUBU9pBJjHOQ=

sdk/solana/executor.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"math"
8+
"regexp"
89

910
"github.com/ethereum/go-ethereum/common"
1011
"github.com/gagliardetto/solana-go"
@@ -19,6 +20,8 @@ import (
1920

2021
var _ sdk.Executor = (*Executor)(nil)
2122

23+
const maxPreloadSignaturesAttempts = 3
24+
2225
// Executor is an Executor implementation for Solana chains, allowing for the execution of
2326
// operations on the MCMS contract
2427
type Executor struct {
@@ -161,7 +164,7 @@ func (e *Executor) SetRoot(
161164
return types.TransactionResult{}, err
162165
}
163166

164-
err = e.preloadSignatures(ctx, pdaSeed, root, validUntil, sortedSignatures, rootSignaturesPDA)
167+
err = e.preloadSignatures(ctx, pdaSeed, root, validUntil, sortedSignatures, rootSignaturesPDA, 0)
165168
if err != nil {
166169
return types.TransactionResult{}, err
167170
}
@@ -200,11 +203,16 @@ func (e *Executor) preloadSignatures(
200203
validUntil uint32,
201204
sortedSignatures []types.Signature,
202205
signaturesPDA solana.PublicKey,
206+
attempt int,
203207
) error {
204208
initSignaturesInstruction := mcm.NewInitSignaturesInstruction(mcmName, root, validUntil,
205209
uint8(len(sortedSignatures)), signaturesPDA, e.auth.PublicKey(), solana.SystemProgramID) //nolint:gosec
206210
_, _, err := e.sendAndConfirm(ctx, e.client, e.auth, initSignaturesInstruction, rpc.CommitmentConfirmed)
207211
if err != nil {
212+
if isAccountAlreadyInUseError(err) {
213+
return e.retryPreloadSignatures(ctx, mcmName, root, validUntil, sortedSignatures, signaturesPDA, attempt)
214+
}
215+
208216
return fmt.Errorf("unable to initialize signatures: %w", err)
209217
}
210218

@@ -229,6 +237,30 @@ func (e *Executor) preloadSignatures(
229237
return nil
230238
}
231239

240+
// retryPreloadSignatures clears the signatures pda and then calls preloadSignatures
241+
func (e *Executor) retryPreloadSignatures(
242+
ctx context.Context,
243+
mcmName [32]byte,
244+
root [32]byte,
245+
validUntil uint32,
246+
sortedSignatures []types.Signature,
247+
signaturesPDA solana.PublicKey,
248+
attempt int,
249+
) error {
250+
if attempt >= maxPreloadSignaturesAttempts {
251+
return fmt.Errorf("maximum attempts to retry preload signatures reached (%d); aborting", attempt)
252+
}
253+
254+
clearSignaturesInstruction := mcm.NewClearSignaturesInstruction(mcmName, root, validUntil, signaturesPDA,
255+
e.auth.PublicKey())
256+
_, _, err := e.sendAndConfirm(ctx, e.client, e.auth, clearSignaturesInstruction, rpc.CommitmentConfirmed)
257+
if err != nil {
258+
return fmt.Errorf("unable to clear signatures: %w", err)
259+
}
260+
261+
return e.preloadSignatures(ctx, mcmName, root, validUntil, sortedSignatures, signaturesPDA, attempt+1)
262+
}
263+
232264
// solanaMetadata returns the root metadata input for the MCM program
233265
func (e *Executor) solanaMetadata(metadata types.ChainMetadata, configPDA [32]byte) mcm.RootMetadataInput {
234266
return mcm.RootMetadataInput{
@@ -264,3 +296,9 @@ func solanaSignatures(signatures []types.Signature) []mcm.Signature {
264296

265297
return solanaSignatures
266298
}
299+
300+
var accountAlreadyInUsePattern = regexp.MustCompile(`Allocate: account Address.*already in use`)
301+
302+
func isAccountAlreadyInUseError(err error) bool {
303+
return accountAlreadyInUsePattern.MatchString(err.Error())
304+
}

sdk/solana/executor_test.go

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ func TestExecutor_ExecuteOperation(t *testing.T) {
9999
},
100100
},
101101
mockSetup: func(m *mocks.JSONRPCClient) {
102-
103102
},
104103
want: "",
105104
wantErr: errors.New("invalid contract ID provided"),
@@ -155,7 +154,6 @@ func TestExecutor_ExecuteOperation(t *testing.T) {
155154
},
156155
},
157156
mockSetup: func(m *mocks.JSONRPCClient) {
158-
159157
},
160158
want: "",
161159
wantErr: errors.New("invalid contract ID provided"),
@@ -216,7 +214,7 @@ func TestExecutor_SetRoot(t *testing.T) {
216214
wantErr string
217215
}{
218216
{
219-
name: "success",
217+
name: "success - direct",
220218
metadata: defaultMetadata,
221219
proof: defaultProof,
222220
root: defaultRoot,
@@ -238,6 +236,42 @@ func TestExecutor_SetRoot(t *testing.T) {
238236
},
239237
want: "oaV9FKKPDVneUANQ9hJqEuhgwfUgbxucUC4TmzpgGJhuSxBueapWc9HJ4cJQMqT2PPQX6rhTbKnXkebsaravnLo",
240238
},
239+
{
240+
name: "success - after account already in use error",
241+
metadata: defaultMetadata,
242+
proof: defaultProof,
243+
root: defaultRoot,
244+
validUntil: defaultValidUntil,
245+
signatures: defaultSignatures,
246+
setup: func(t *testing.T, executor *Executor, mockJSONRPCClient *mocks.JSONRPCClient) {
247+
t.Helper()
248+
249+
accountAlreadyInUseError := errors.New(`
250+
(string) (len=4) "logs": ([]interface {}) (len=7 cap=8) {
251+
(string) (len=63) "Program 5vNJx78mz7KVMjhuipyr9jKBKcMrKYGdjGkgE4LUmjKk invoke [1]",
252+
(string) (len=40) "Program log: Instruction: InitSignatures",
253+
(string) (len=51) "Program 11111111111111111111111111111111 invoke [2]",
254+
(string) (len=110) "Allocate: account Address { address: DKvwoHhcRMZwsedUvJqmsNuat52WdcYQD7agqMjJybbF, base: None } already in use",
255+
(string) (len=74) "Program 11111111111111111111111111111111 failed: custom program error: 0x0",
256+
(string) (len=90) "Program 5vNJx78mz7KVMjhuipyr9jKBKcMrKYGdjGkgE4LUmjKk consumed 7202 of 200000 compute units",
257+
(string) (len=86) "Program 5vNJx78mz7KVMjhuipyr9jKBKcMrKYGdjGkgE4LUmjKk failed: custom program error: 0x0"`)
258+
259+
// 6 transactions: init-signatures, clear-signatures, init-signatures append-signatures, finalize-signatures, set-root
260+
mockSolanaTransaction(t, mockJSONRPCClient, 80, 70,
261+
"AxzwxQ2DLR4zEFxEPGaafR4z3MY4CP1CAdSs1ZZhArtgS3G4F9oYSy3Nx1HyA1Macb4bYEi4jU6F1CL4SRrZz1v", nil, accountAlreadyInUseError)
262+
mockSolanaTransaction(t, mockJSONRPCClient, 81, 71,
263+
"5qm3BUCF1DswRm4r32mipWZa5NrbHYgPst8BJXr1BysNaqfEz4kVGnGzCx3vLWJoWi2FRszzgLUxfcmAfLzzHw9n", nil, nil)
264+
mockSolanaTransaction(t, mockJSONRPCClient, 82, 72,
265+
"KCAkcwG8LG3cS3bUemdR1grv6EoyqfgDJN3BJfN58azEDkpRFa9S66RDFvtNWHga9htimSnfNkGoWVLLx7AJKrs", nil, nil)
266+
mockSolanaTransaction(t, mockJSONRPCClient, 83, 63,
267+
"4GWCdxQAsAmbqyaB1SCHpmnyRWmBwBY5S6hUpSMz2gENErgt8zqmsXnz9dbXCchucZqAf7ZdctzTNtUUTjD8rMcv", nil, nil)
268+
mockSolanaTransaction(t, mockJSONRPCClient, 84, 64,
269+
"npfXXzfJzSkB6QcwS36P9dzc61itiDnLX9yGVyzaooftiSFs1A53JWHGQq6F9MzjWxKD8bRLPKWuGseUxHK56s3", nil, nil)
270+
mockSolanaTransaction(t, mockJSONRPCClient, 85, 65,
271+
"2W1d6qQAVPjssJgm6WMeLxwatKZ9TUUZcvNHdFaguvLxrCFqPdpuikiz9YdfkXG5eLojQNjrW6L6W2sFRWEujER", nil, nil)
272+
},
273+
want: "2W1d6qQAVPjssJgm6WMeLxwatKZ9TUUZcvNHdFaguvLxrCFqPdpuikiz9YdfkXG5eLojQNjrW6L6W2sFRWEujER",
274+
},
241275
{
242276
name: "failure: invalid address",
243277
metadata: types.ChainMetadata{StartingOpCount: 100, MCMAddress: "invalid-mcm-address"},
@@ -377,3 +411,42 @@ func newTestExecutor(t *testing.T, auth solana.PrivateKey, chainSelector types.C
377411

378412
return NewExecutor(encoder, client, auth), mockJSONRPCClient
379413
}
414+
415+
func Test_isAccountAlreadyInUseError(t *testing.T) {
416+
t.Parallel()
417+
418+
tests := []struct {
419+
name string
420+
err error
421+
want bool
422+
}{
423+
{
424+
name: "account already in use error",
425+
err: errors.New(`
426+
(string) (len=51) "Program 11111111111111111111111111111111 invoke [2]",
427+
(string) (len=110) "Allocate: account Address { address: DKvwoHhcRMZwsedUvJqmsNuat52WdcYQD7agqMjJybbF, base: None } already in use",
428+
(string) (len=74) "Program 11111111111111111111111111111111 failed: custom program error: 0x0",
429+
(string) (len=90) "Program 5vNJx78mz7KVMjhuipyr9jKBKcMrKYGdjGkgE4LUmjKk consumed 7202 of 200000 compute units",
430+
`),
431+
want: true,
432+
},
433+
{
434+
name: "other error",
435+
err: errors.New(`other error`),
436+
want: false,
437+
},
438+
{
439+
name: "other similar but not quite the same error",
440+
err: errors.New(`Allocate: already in use`),
441+
want: false,
442+
},
443+
}
444+
for _, tt := range tests {
445+
t.Run(tt.name, func(t *testing.T) {
446+
t.Parallel()
447+
448+
got := isAccountAlreadyInUseError(tt.err)
449+
require.Equal(t, tt.want, got)
450+
})
451+
}
452+
}

0 commit comments

Comments
 (0)