Skip to content

Commit

Permalink
DPA-1413: fix(solana): IsOperation in timelock inspector (#231)
Browse files Browse the repository at this point in the history
Implement status operation check for timelock inspector for solana. 

e2e tests will come in another PR as that involves more setup and work.

JIRA: https://smartcontract-it.atlassian.net/browse/DPA-1413
<!-- DON'T DELETE. add your comments above llm generated contents -->
---
**Below is a summarization created by an LLM (gpt-4o-2024-08-06). Be
mindful of hallucinations and verify accuracy.**

## Why
Introduces a new `TimelockInspector` for Solana chains to access the
RBACTimelock contract, providing functionalities to check operation
status and manage timelock operations effectively.

## What
- **timelock_inspector.go**
  - Added `TimelockInspector` struct and methods for Solana chains
  - Implemented `IsOperation` to check operation existence
  - Implemented `IsOperationPending` to check pending status
  - Implemented `IsOperationReady` to check readiness status
  - Implemented `IsOperationDone` to check completion status
  - Added helper function `getOpData` to retrieve operation data
  - Added `safeConvertUint64ToInt64` for safe type conversion
  • Loading branch information
graham-chainlink authored Jan 14, 2025
1 parent 563f3fe commit a8447e1
Show file tree
Hide file tree
Showing 9 changed files with 516 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-queens-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": minor
---

feat(solana): timelock inspection - operation statuses check
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52
github.com/smartcontractkit/chain-selectors v1.0.35
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250103152858-8973fd0c912b
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250110192108-679d6f555e8d
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.1
github.com/spf13/cast v1.7.1
github.com/stretchr/testify v1.10.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ github.com/smartcontractkit/chain-selectors v1.0.35 h1:k7iJqChFbH10WFpahjDtDJoYy
github.com/smartcontractkit/chain-selectors v1.0.35/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250103152858-8973fd0c912b h1:UBXi9Yj8YSMHDDaxQLu273x1fWjyEL9xP58nuJsqZfg=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250103152858-8973fd0c912b/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250110192108-679d6f555e8d h1:kdn+WtHqE3j+KApNIgiRAxzWmdFRPZJ9CeRjP2QU0vk=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250110192108-679d6f555e8d/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-common v0.4.0 h1:GZ9MhHt5QHXSaK/sAZvKDxkEqF4fPiFHWHEPqs/2C2o=
github.com/smartcontractkit/chainlink-common v0.4.0/go.mod h1:yti7e1+G9hhkYhj+L5sVUULn9Bn3bBL5/AxaNqdJ5YQ=
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.1 h1:573e5JlpGOjY/RDJziG62Cw5D66xghWHJcmfWvv6yc4=
Expand Down
12 changes: 6 additions & 6 deletions sdk/solana/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,20 @@ func FindSeenSignedHashesPDA(
}

func FindTimelockConfigPDA(
programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_config"), pdaSeed[:]}
programID solana.PublicKey, timelockID PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_config"), timelockID[:]}
return findPDA(programID, seeds)
}

func FindTimelockOperationPDA(
programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_operation"), pdaSeed[:]}
programID solana.PublicKey, timelockID PDASeed, opID [32]byte) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_operation"), timelockID[:], opID[:]}
return findPDA(programID, seeds)
}

func FindTimelockSignerPDA(
programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_signer"), pdaSeed[:]}
programID solana.PublicKey, timelockID PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_signer"), timelockID[:]}
return findPDA(programID, seeds)
}

Expand Down
5 changes: 3 additions & 2 deletions sdk/solana/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
var (
testChainSelector = types.ChainSelector(cselectors.SOLANA_DEVNET.Selector)
testProgramID = solana.MustPublicKeyFromBase58("6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX")
testOpID = [32]byte{1, 2, 3, 4}
testPDASeed = PDASeed{'t', 'e', 's', 't', '-', 'm', 'c', 'm'}
testRoot = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")
)
Expand Down Expand Up @@ -82,9 +83,9 @@ func Test_FindTimelockConfigPDA(t *testing.T) {

func Test_FindTimelockOperationPDA(t *testing.T) {
t.Parallel()
pda, err := FindTimelockOperationPDA(testProgramID, testPDASeed)
pda, err := FindTimelockOperationPDA(testProgramID, testPDASeed, testOpID)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("8TL4xwjpntLQXeFbADMPnooDofGUwocc4ikHAJb41Fcm")))
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("8iy9TQJB1jBBMBmwmiguZDRZpbDQNuvRCQu4cfdpgbKy")))
}

func Test_FindTimelockSignerPDA(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions sdk/solana/inspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ func TestInspector_GetConfig(t *testing.T) {
name: "success",
setup: func(mockJSONRPCClient *mocks.JSONRPCClient) {
mcmConfig := &bindings.MultisigConfig{
ChainId: chainSelector,
MultisigName: testPDASeed,
Owner: solana.SystemProgramID,
ChainId: chainSelector,
MultisigId: testPDASeed,
Owner: solana.SystemProgramID,
Signers: []bindings.McmSigner{
{EvmAddress: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdef"), Index: 0, Group: 0},
{EvmAddress: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), Index: 1, Group: 0},
Expand Down
131 changes: 131 additions & 0 deletions sdk/solana/timelock_inspector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package solana

import (
"context"
"errors"

"github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go/rpc"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock"
solanaCommon "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common"

"github.com/smartcontractkit/mcms/sdk"
)

var _ sdk.TimelockInspector = (*TimelockInspector)(nil)

var TimelockOpDoneTimestamp = uint64(1)

// TimelockInspector is an Inspector implementation for Solana chains for accessing the RBACTimelock contract
type TimelockInspector struct {
client *rpc.Client
}

// NewTimelockInspector creates a new TimelockInspector
func NewTimelockInspector(client *rpc.Client) *TimelockInspector {
return &TimelockInspector{client: client}
}

func (t TimelockInspector) GetProposers(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) GetExecutors(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) GetBypassers(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) GetCancellers(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) IsOperation(ctx context.Context, address string, opID [32]byte) (bool, error) {
_, err := t.getOpData(ctx, address, opID)
if err != nil {
if errors.Is(err, rpc.ErrNotFound) {
return false, nil
}

return false, err
}

return true, nil
}

func (t TimelockInspector) IsOperationPending(ctx context.Context, address string, opID [32]byte) (bool, error) {
op, err := t.getOpData(ctx, address, opID)
if err != nil {
return false, err
}

return op.Timestamp > TimelockOpDoneTimestamp, nil
}

func (t TimelockInspector) IsOperationReady(ctx context.Context, address string, opID [32]byte) (bool, error) {
op, err := t.getOpData(ctx, address, opID)
if err != nil {
return false, err
}

blockTime, err := solanaCommon.GetBlockTime(ctx, t.client, rpc.CommitmentConfirmed)
if err != nil {
return false, err
}

ts, err := safeConvertUint64ToInt64(op.Timestamp)
if err != nil {
return false, err
}

// a safety catch to prevent nil blocktime - ideally should not happen
if blockTime == nil {
return false, errors.New("failed to get block time: nil value")
}

return op.Timestamp > TimelockOpDoneTimestamp && ts <= int64(*blockTime), nil
}

func (t TimelockInspector) IsOperationDone(ctx context.Context, address string, opID [32]byte) (bool, error) {
op, err := t.getOpData(ctx, address, opID)
if err != nil {
return false, err
}

return op.Timestamp == TimelockOpDoneTimestamp, nil
}

func (t TimelockInspector) getOpData(ctx context.Context, address string, opID [32]byte) (timelock.Operation, error) {
programID, seed, err := ParseContractAddress(address)
if err != nil {
return timelock.Operation{}, err
}

mcm.SetProgramID(programID)

pda, err := FindTimelockOperationPDA(programID, seed, opID)
if err != nil {
return timelock.Operation{}, err
}

var opAccount timelock.Operation
err = solanaCommon.GetAccountDataBorshInto(ctx, t.client, pda, rpc.CommitmentConfirmed, &opAccount)
if err != nil {
return timelock.Operation{}, err
}

return opAccount, nil
}

func safeConvertUint64ToInt64(value uint64) (int64, error) {
const maxInt64 = int64(^uint64(0) >> 1)

if value > uint64(maxInt64) {
return 0, errors.New("cannot convert uint64 to int64: value out of int64 range")
}

return int64(value), nil
}
Loading

0 comments on commit a8447e1

Please sign in to comment.