-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Solana plugin codec implementation #15816
Changes from 90 commits
71b06dc
c79a406
0fe505c
de95bf1
e51bd46
86c9c17
237e4f4
7ae43ed
4e2ea85
0d49c2e
5849b3b
70db87e
d2a7a39
ae3e43c
11cd2f3
40abc5c
4e2f647
558c0ff
07acdb1
0c0b802
f0a5a0e
22ad68c
effd030
26e9cb3
b988745
56d85d5
75ae059
0272e75
465d69c
f314edc
a712c88
8c02e8b
bad1acb
6023de7
23d9c80
82185a0
8f42923
4fdf7f7
b0b8d31
85dfafd
9ec6505
a5b661d
e169af2
a52044d
3f4929a
d5413ae
03ae3ee
6ecbec2
67d689c
e9abe63
9497f40
e9ae48d
9c4fff2
12ee1f4
443cfe1
c60a925
24e9fa3
912b91c
03787a8
25d2acd
48464b0
6f80799
58a9497
6ea469d
fee6bb6
2d79cb0
80496c9
d856147
ed8cd1c
1bcb320
3ec1574
2b79eac
626a423
32956a6
9779bd2
9822b54
12cd5c9
e03c253
283c4e4
1ede47a
d7e246f
3598eaa
3a1b986
8a14890
fd469bb
c088d5c
ff083a4
57999be
951498b
0c91600
7e72097
0589fb8
40ea8f7
97a8278
7bdb472
59fc6b5
7193324
761eb11
a8f4eea
e04db54
8f5a842
bb84f23
d88f48e
a459033
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"chainlink": minor | ||
--- | ||
|
||
Solana CCIP plugin codec support for both commit and execute report #added |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package ccipsolana | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"math/big" | ||
|
||
agbinary "github.com/gagliardetto/binary" | ||
"github.com/gagliardetto/solana-go" | ||
|
||
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" | ||
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" | ||
) | ||
|
||
// CommitPluginCodecV1 is a codec for encoding and decoding commit plugin reports. | ||
// Compatible with: | ||
// - "OffRamp 1.6.0-dev" | ||
type CommitPluginCodecV1 struct{} | ||
|
||
func NewCommitPluginCodecV1() *CommitPluginCodecV1 { | ||
return &CommitPluginCodecV1{} | ||
} | ||
|
||
func (c *CommitPluginCodecV1) Encode(ctx context.Context, report cciptypes.CommitPluginReport) ([]byte, error) { | ||
var buf bytes.Buffer | ||
encoder := agbinary.NewBorshEncoder(&buf) | ||
if len(report.MerkleRoots) != 1 { | ||
return nil, fmt.Errorf("unexpected merkle root length in report: %d", len(report.MerkleRoots)) | ||
} | ||
|
||
mr := ccip_router.MerkleRoot{ | ||
SourceChainSelector: uint64(report.MerkleRoots[0].ChainSel), | ||
OnRampAddress: report.MerkleRoots[0].OnRampAddress, | ||
MinSeqNr: uint64(report.MerkleRoots[0].SeqNumsRange.Start()), | ||
MaxSeqNr: uint64(report.MerkleRoots[0].SeqNumsRange.End()), | ||
MerkleRoot: report.MerkleRoots[0].MerkleRoot, | ||
} | ||
|
||
tpu := make([]ccip_router.TokenPriceUpdate, 0, len(report.PriceUpdates.TokenPriceUpdates)) | ||
for _, update := range report.PriceUpdates.TokenPriceUpdates { | ||
token, err := solana.PublicKeyFromBase58(string(update.TokenID)) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid token address: %s, %w", update.TokenID, err) | ||
} | ||
if update.Price.IsEmpty() { | ||
return nil, fmt.Errorf("empty price for token: %s", update.TokenID) | ||
} | ||
tpu = append(tpu, ccip_router.TokenPriceUpdate{ | ||
SourceToken: token, | ||
UsdPerToken: [28]uint8(encodeBigIntToFixedLengthLE(update.Price.Int, 28)), | ||
}) | ||
} | ||
|
||
gpu := make([]ccip_router.GasPriceUpdate, 0, len(report.PriceUpdates.GasPriceUpdates)) | ||
for _, update := range report.PriceUpdates.GasPriceUpdates { | ||
if update.GasPrice.IsEmpty() { | ||
return nil, fmt.Errorf("empty gas price for chain: %d", update.ChainSel) | ||
} | ||
|
||
gpu = append(gpu, ccip_router.GasPriceUpdate{ | ||
DestChainSelector: uint64(update.ChainSel), | ||
UsdPerUnitGas: [28]uint8(encodeBigIntToFixedLengthLE(update.GasPrice.Int, 28)), | ||
}) | ||
} | ||
|
||
commit := ccip_router.CommitInput{ | ||
MerkleRoot: mr, | ||
PriceUpdates: ccip_router.PriceUpdates{ | ||
TokenPriceUpdates: tpu, | ||
GasPriceUpdates: gpu, | ||
}, | ||
} | ||
|
||
err := commit.MarshalWithEncoder(encoder) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return buf.Bytes(), nil | ||
} | ||
|
||
func (c *CommitPluginCodecV1) Decode(ctx context.Context, bytes []byte) (cciptypes.CommitPluginReport, error) { | ||
decoder := agbinary.NewBorshDecoder(bytes) | ||
commitReport := ccip_router.CommitInput{} | ||
err := commitReport.UnmarshalWithDecoder(decoder) | ||
if err != nil { | ||
return cciptypes.CommitPluginReport{}, err | ||
} | ||
|
||
merkleRoots := []cciptypes.MerkleRootChain{ | ||
{ | ||
ChainSel: cciptypes.ChainSelector(commitReport.MerkleRoot.SourceChainSelector), | ||
OnRampAddress: commitReport.MerkleRoot.OnRampAddress, | ||
SeqNumsRange: cciptypes.NewSeqNumRange( | ||
cciptypes.SeqNum(commitReport.MerkleRoot.MinSeqNr), | ||
cciptypes.SeqNum(commitReport.MerkleRoot.MaxSeqNr), | ||
), | ||
MerkleRoot: commitReport.MerkleRoot.MerkleRoot, | ||
}, | ||
} | ||
|
||
tokenPriceUpdates := make([]cciptypes.TokenPrice, 0, len(commitReport.PriceUpdates.TokenPriceUpdates)) | ||
for _, update := range commitReport.PriceUpdates.TokenPriceUpdates { | ||
tokenPriceUpdates = append(tokenPriceUpdates, cciptypes.TokenPrice{ | ||
TokenID: cciptypes.UnknownEncodedAddress(update.SourceToken.String()), | ||
Price: decodeLEToBigInt(update.UsdPerToken[:]), | ||
}) | ||
} | ||
|
||
gasPriceUpdates := make([]cciptypes.GasPriceChain, 0, len(commitReport.PriceUpdates.GasPriceUpdates)) | ||
for _, update := range commitReport.PriceUpdates.GasPriceUpdates { | ||
gasPriceUpdates = append(gasPriceUpdates, cciptypes.GasPriceChain{ | ||
GasPrice: decodeLEToBigInt(update.UsdPerUnitGas[:]), | ||
ChainSel: cciptypes.ChainSelector(update.DestChainSelector), | ||
}) | ||
} | ||
|
||
return cciptypes.CommitPluginReport{ | ||
MerkleRoots: merkleRoots, | ||
PriceUpdates: cciptypes.PriceUpdates{ | ||
TokenPriceUpdates: tokenPriceUpdates, | ||
GasPriceUpdates: gasPriceUpdates, | ||
}, | ||
}, nil | ||
} | ||
|
||
func encodeBigIntToFixedLengthLE(bi *big.Int, length int) []byte { | ||
// Create a fixed-length byte array | ||
paddedBytes := make([]byte, length) | ||
|
||
// Use FillBytes to fill the array with big-endian data, zero-padded | ||
bi.FillBytes(paddedBytes) | ||
|
||
// Reverse the array for little-endian encoding | ||
for i, j := 0, len(paddedBytes)-1; i < j; i, j = i+1, j-1 { | ||
paddedBytes[i], paddedBytes[j] = paddedBytes[j], paddedBytes[i] | ||
} | ||
|
||
return paddedBytes | ||
} | ||
|
||
func decodeLEToBigInt(data []byte) cciptypes.BigInt { | ||
// Reverse the byte array to convert it from little-endian to big-endian | ||
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { | ||
data[i], data[j] = data[j], data[i] | ||
} | ||
|
||
// Use big.Int.SetBytes to construct the big.Int | ||
bi := new(big.Int).SetBytes(data) | ||
if bi.Int64() == 0 { | ||
return cciptypes.NewBigInt(big.NewInt(0)) | ||
} | ||
|
||
return cciptypes.NewBigInt(bi) | ||
} | ||
|
||
// Ensure CommitPluginCodec implements the CommitPluginCodec interface | ||
var _ cciptypes.CommitPluginCodec = (*CommitPluginCodecV1)(nil) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
package ccipsolana | ||
|
||
import ( | ||
"bytes" | ||
"math/big" | ||
"math/rand" | ||
"strconv" | ||
"testing" | ||
|
||
agbinary "github.com/gagliardetto/binary" | ||
solanago "github.com/gagliardetto/solana-go" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" | ||
|
||
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" | ||
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils" | ||
"github.com/smartcontractkit/chainlink/v2/evm/utils" | ||
) | ||
|
||
var randomCommitReport = func() cciptypes.CommitPluginReport { | ||
pubkey, err := solanago.NewRandomPrivateKey() | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
return cciptypes.CommitPluginReport{ | ||
MerkleRoots: []cciptypes.MerkleRootChain{ | ||
{ | ||
OnRampAddress: cciptypes.UnknownAddress(pubkey.PublicKey().String()), | ||
ChainSel: cciptypes.ChainSelector(rand.Uint64()), | ||
SeqNumsRange: cciptypes.NewSeqNumRange( | ||
cciptypes.SeqNum(rand.Uint64()), | ||
cciptypes.SeqNum(rand.Uint64()), | ||
), | ||
MerkleRoot: utils.RandomBytes32(), | ||
}, | ||
}, | ||
PriceUpdates: cciptypes.PriceUpdates{ | ||
TokenPriceUpdates: []cciptypes.TokenPrice{ | ||
{ | ||
TokenID: "C8WSPj3yyus1YN3yNB6YA5zStYtbjQWtpmKadmvyUXq8", | ||
Price: cciptypes.NewBigInt(big.NewInt(rand.Int63())), | ||
}, | ||
}, | ||
GasPriceUpdates: []cciptypes.GasPriceChain{ | ||
{GasPrice: cciptypes.NewBigInt(big.NewInt(rand.Int63())), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, | ||
{GasPrice: cciptypes.NewBigInt(big.NewInt(rand.Int63())), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, | ||
{GasPrice: cciptypes.NewBigInt(big.NewInt(rand.Int63())), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func TestCommitPluginCodecV1(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just noticed that the EVM codec tests don't decode using the onchain contracts, which is probably an oversight since the exec codec does. Is there any way we can achieve the same coverage w/ this Solana encoder? Presumably the Solana contract will attempt to decode the commit report bytes into some data structure, having that covered here means we're good to go and covers an important part of the E2E flow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I think it might be possible. |
||
testCases := []struct { | ||
name string | ||
report func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport | ||
expErr bool | ||
}{ | ||
{ | ||
name: "base report", | ||
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { | ||
return report | ||
}, | ||
}, | ||
{ | ||
name: "empty token address", | ||
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { | ||
report.PriceUpdates.TokenPriceUpdates[0].TokenID = "" | ||
return report | ||
}, | ||
expErr: true, | ||
}, | ||
{ | ||
name: "empty merkle root", | ||
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { | ||
report.MerkleRoots[0].MerkleRoot = cciptypes.Bytes32{} | ||
return report | ||
}, | ||
}, | ||
{ | ||
name: "zero token price", | ||
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { | ||
report.PriceUpdates.TokenPriceUpdates[0].Price = cciptypes.NewBigInt(big.NewInt(0)) | ||
return report | ||
}, | ||
}, | ||
{ | ||
name: "zero gas price", | ||
huangzhen1997 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { | ||
report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(big.NewInt(0)) | ||
return report | ||
}, | ||
}, | ||
{ | ||
name: "empty gas price", | ||
report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { | ||
report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(nil) | ||
return report | ||
}, | ||
expErr: true, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
report := tc.report(randomCommitReport()) | ||
commitCodec := NewCommitPluginCodecV1() | ||
ctx := testutils.Context(t) | ||
encodedReport, err := commitCodec.Encode(ctx, report) | ||
if tc.expErr { | ||
assert.Error(t, err) | ||
return | ||
} | ||
require.NoError(t, err) | ||
decodedReport, err := commitCodec.Decode(ctx, encodedReport) | ||
require.NoError(t, err) | ||
require.Equal(t, report, decodedReport) | ||
}) | ||
} | ||
} | ||
|
||
func BenchmarkCommitPluginCodecV1_Encode(b *testing.B) { | ||
huangzhen1997 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
commitCodec := NewCommitPluginCodecV1() | ||
ctx := testutils.Context(b) | ||
|
||
rep := randomCommitReport() | ||
for i := 0; i < b.N; i++ { | ||
_, err := commitCodec.Encode(ctx, rep) | ||
require.NoError(b, err) | ||
} | ||
} | ||
|
||
func BenchmarkCommitPluginCodecV1_Decode(b *testing.B) { | ||
commitCodec := NewCommitPluginCodecV1() | ||
ctx := testutils.Context(b) | ||
encodedReport, err := commitCodec.Encode(ctx, randomCommitReport()) | ||
require.NoError(b, err) | ||
|
||
for i := 0; i < b.N; i++ { | ||
_, err := commitCodec.Decode(ctx, encodedReport) | ||
require.NoError(b, err) | ||
} | ||
} | ||
|
||
func BenchmarkCommitPluginCodecV1_Encode_Decode(b *testing.B) { | ||
commitCodec := NewCommitPluginCodecV1() | ||
ctx := testutils.Context(b) | ||
|
||
rep := randomCommitReport() | ||
for i := 0; i < b.N; i++ { | ||
encodedReport, err := commitCodec.Encode(ctx, rep) | ||
require.NoError(b, err) | ||
decodedReport, err := commitCodec.Decode(ctx, encodedReport) | ||
require.NoError(b, err) | ||
require.Equal(b, rep, decodedReport) | ||
} | ||
} | ||
|
||
func Test_DecodingCommitReport(t *testing.T) { | ||
t.Run("decode on-chain commit report", func(t *testing.T) { | ||
chainSel := cciptypes.ChainSelector(rand.Uint64()) | ||
minSeqNr := rand.Uint64() | ||
maxSeqNr := minSeqNr + 10 | ||
onRampAddr, err := solanago.NewRandomPrivateKey() | ||
require.NoError(t, err) | ||
|
||
tokenSource := solanago.MustPublicKeyFromBase58("C8WSPj3yyus1YN3yNB6YA5zStYtbjQWtpmKadmvyUXq8") | ||
tokenPrice := encodeBigIntToFixedLengthLE(big.NewInt(rand.Int63()), 28) | ||
gasPrice := encodeBigIntToFixedLengthLE(big.NewInt(rand.Int63()), 28) | ||
merkleRoot := utils.RandomBytes32() | ||
|
||
tpu := []ccip_router.TokenPriceUpdate{ | ||
{ | ||
SourceToken: tokenSource, | ||
UsdPerToken: [28]uint8(tokenPrice), | ||
}, | ||
} | ||
|
||
gpu := []ccip_router.GasPriceUpdate{ | ||
{UsdPerUnitGas: [28]uint8(gasPrice), DestChainSelector: uint64(chainSel)}, | ||
{UsdPerUnitGas: [28]uint8(gasPrice), DestChainSelector: uint64(chainSel)}, | ||
{UsdPerUnitGas: [28]uint8(gasPrice), DestChainSelector: uint64(chainSel)}, | ||
} | ||
|
||
onChainReport := ccip_router.CommitInput{ | ||
MerkleRoot: ccip_router.MerkleRoot{ | ||
SourceChainSelector: uint64(chainSel), | ||
OnRampAddress: onRampAddr.PublicKey().Bytes(), | ||
MinSeqNr: minSeqNr, | ||
MaxSeqNr: maxSeqNr, | ||
MerkleRoot: merkleRoot, | ||
}, | ||
PriceUpdates: ccip_router.PriceUpdates{ | ||
TokenPriceUpdates: tpu, | ||
GasPriceUpdates: gpu, | ||
}, | ||
} | ||
|
||
var buf bytes.Buffer | ||
encoder := agbinary.NewBorshEncoder(&buf) | ||
err = onChainReport.MarshalWithEncoder(encoder) | ||
require.NoError(t, err) | ||
|
||
commitCodec := NewCommitPluginCodecV1() | ||
decode, err := commitCodec.Decode(testutils.Context(t), buf.Bytes()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we also do the reverse for the same data? |
||
require.NoError(t, err) | ||
mr := decode.MerkleRoots[0] | ||
|
||
// check decoded ocr report merkle root matches with on-chain report | ||
require.Equal(t, strconv.FormatUint(minSeqNr, 10), mr.SeqNumsRange.Start().String()) | ||
require.Equal(t, strconv.FormatUint(maxSeqNr, 10), mr.SeqNumsRange.End().String()) | ||
require.Equal(t, cciptypes.UnknownAddress(onRampAddr.PublicKey().Bytes()), mr.OnRampAddress) | ||
require.Equal(t, cciptypes.Bytes32(merkleRoot), mr.MerkleRoot) | ||
|
||
// check decoded ocr report token price update matches with on-chain report | ||
pu := decode.PriceUpdates.TokenPriceUpdates[0] | ||
require.Equal(t, decodeLEToBigInt(tokenPrice), pu.Price) | ||
require.Equal(t, cciptypes.UnknownEncodedAddress(tokenSource.String()), pu.TokenID) | ||
|
||
// check decoded ocr report gas price update matches with on-chain report | ||
gu := decode.PriceUpdates.GasPriceUpdates[0] | ||
require.Equal(t, decodeLEToBigInt(gasPrice), gu.GasPrice) | ||
require.Equal(t, chainSel, gu.ChainSel) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why can't we use the Solana Codec for this? Just define the IDL for these types, I haven't looked closely into the types being encodec, so I may be missing something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could try Solana codec, @archseer suggested to use the generated gobinding for this earlier, and I also think it makes sense as the EVM codec right now is using similar approach with the generated type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a general topic earlier, where we said lets use the Codec for both EVM and Solana, to make this code here chain-abstract.
But it was more work, and complex, so we abandoned that approach to be looked at at a longer term.