Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions capabilities/blockchain/solana/bindings/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package bindings

import (
"bytes"
"crypto/sha256"
"fmt"

binary "github.com/gagliardetto/binary"

"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana"
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know enough about Solana to review this, but are you planning on having mock helpers like EVM does that the bindings can use to generate mocks for your contract? If so, is it going to be in another PR?

Essentially, the EVM one allows you to bind your contract to an EVM client, so you can simulate the read / write using the types that the contract would receive and respond with instead of requiring you decode and encode. Also, it ensures your'll calling the right contract if you bind to multiple.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// ForwarderReport represents the Borsh-serialized report format expected by
// the Solana keystone-forwarder program's on_report instruction.
type ForwarderReport struct {
AccountHash [32]byte
Payload []byte
}

// MarshalWithEncoder serializes the ForwarderReport using the provided Borsh encoder.
func (obj ForwarderReport) MarshalWithEncoder(encoder *binary.Encoder) (err error) {
// Serialize `AccountHash`:
err = encoder.Encode(obj.AccountHash)
if err != nil {
return fmt.Errorf("field AccountHash: %w", err)
}
// Serialize `Payload`:
err = encoder.Encode(obj.Payload)
if err != nil {
return fmt.Errorf("field Payload: %w", err)
}
return nil
}

// Marshal serializes the ForwarderReport into Borsh-encoded bytes.
func (obj ForwarderReport) Marshal() ([]byte, error) {
buf := bytes.NewBuffer(nil)
encoder := binary.NewBorshEncoder(buf)
err := obj.MarshalWithEncoder(encoder)
if err != nil {
return nil, fmt.Errorf("error while encoding ForwarderReport: %w", err)
}
return buf.Bytes(), nil
}

// CalculateAccountsHash computes the SHA-256 hash of the concatenated public
// keys of the given accounts, matching the on-chain account hash verification.
func CalculateAccountsHash(accs []*solana.AccountMeta) [32]byte {
accounts := make([]byte, 0, len(accs)*32)
for _, acc := range accs {
if acc == nil {
continue
}
accounts = append(accounts, acc.PublicKey...)
}
return sha256.Sum256(accounts)
}
133 changes: 133 additions & 0 deletions capabilities/blockchain/solana/bindings/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package bindings

import (
"crypto/sha256"
"encoding/binary"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana"
)

func TestForwarderReport_Marshal(t *testing.T) {
t.Run("encodes to expected Borsh format", func(t *testing.T) {
hash := [32]byte{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
}
payload := []byte("hello solana")

report := ForwarderReport{
AccountHash: hash,
Payload: payload,
}

data, err := report.Marshal()
require.NoError(t, err)

// Borsh format: account_hash(32) | payload_len(u32 LE) | payload
expectedLen := 32 + 4 + len(payload)
require.Len(t, data, expectedLen)

// First 32 bytes: account hash
assert.Equal(t, hash[:], data[:32])

// Next 4 bytes: little-endian u32 payload length
payloadLen := binary.LittleEndian.Uint32(data[32:36])
assert.Equal(t, uint32(len(payload)), payloadLen)

// Remaining bytes: payload
assert.Equal(t, payload, data[36:])
})
}

func TestCalculateAccountsHash(t *testing.T) {
t.Run("single account", func(t *testing.T) {
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}

accs := []*solana.AccountMeta{
{PublicKey: key},
}

got := CalculateAccountsHash(accs)
expected := sha256.Sum256(key)
assert.Equal(t, expected, got)
})

t.Run("multiple accounts", func(t *testing.T) {
key1 := make([]byte, 32)
key2 := make([]byte, 32)
for i := range key1 {
key1[i] = byte(i)
key2[i] = byte(i + 32)
}

accs := []*solana.AccountMeta{
{PublicKey: key1},
{PublicKey: key2},
}

got := CalculateAccountsHash(accs)

// Hash should be SHA-256 of concatenated keys
concat := append(key1, key2...)
expected := sha256.Sum256(concat)
assert.Equal(t, expected, got)
})

t.Run("empty slice", func(t *testing.T) {
got := CalculateAccountsHash([]*solana.AccountMeta{})
expected := sha256.Sum256([]byte{})
assert.Equal(t, expected, got)
})

t.Run("nil slice", func(t *testing.T) {
got := CalculateAccountsHash(nil)
expected := sha256.Sum256([]byte{})
assert.Equal(t, expected, got)
})

t.Run("skips nil entries", func(t *testing.T) {
key := make([]byte, 32)
for i := range key {
key[i] = byte(i + 100)
}

accs := []*solana.AccountMeta{
nil,
{PublicKey: key},
nil,
}

got := CalculateAccountsHash(accs)
expected := sha256.Sum256(key)
assert.Equal(t, expected, got)
})

t.Run("order matters", func(t *testing.T) {
key1 := make([]byte, 32)
key2 := make([]byte, 32)
for i := range key1 {
key1[i] = byte(i)
key2[i] = byte(i + 32)
}

hash12 := CalculateAccountsHash([]*solana.AccountMeta{
{PublicKey: key1},
{PublicKey: key2},
})
hash21 := CalculateAccountsHash([]*solana.AccountMeta{
{PublicKey: key2},
{PublicKey: key1},
})

assert.NotEqual(t, hash12, hash21, "hash should depend on account order")
})
}
18 changes: 18 additions & 0 deletions capabilities/blockchain/solana/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,30 @@ module github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana
go 1.25.3

require (
github.com/gagliardetto/binary v0.8.0
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9
github.com/smartcontractkit/cre-sdk-go v1.0.0
github.com/stretchr/testify v1.10.0
google.golang.org/protobuf v1.36.7
)

require (
github.com/blendle/zapdriver v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

// Force newer version of golang.org/x/crypto to avoid CVE-2022-27191 and other vulnerabilities
replace golang.org/x/crypto => golang.org/x/crypto v0.47.0
Loading
Loading