From 30f9ac53adec7b8ad223bacb9fd106b68091ae10 Mon Sep 17 00:00:00 2001 From: aalu1418 <50029043+aalu1418@users.noreply.github.com> Date: Thu, 18 Apr 2024 19:19:01 -0600 Subject: [PATCH] parse compute unit price from tx details --- pkg/monitoring/types/txdetails.go | 32 +++++++++----- pkg/monitoring/types/txdetails_test.go | 59 ++++++++++++++++---------- pkg/solana/fees/computebudget.go | 14 ++++++ pkg/solana/fees/computebudget_test.go | 20 +++++++++ 4 files changed, 92 insertions(+), 33 deletions(-) diff --git a/pkg/monitoring/types/txdetails.go b/pkg/monitoring/types/txdetails.go index ce526e8d8..8f04e61a1 100644 --- a/pkg/monitoring/types/txdetails.go +++ b/pkg/monitoring/types/txdetails.go @@ -26,8 +26,9 @@ type TxDetails struct { Sender solanaGo.PublicKey - // report information - only supports single report per tx + // report tx information - only supports single report per tx ObservationCount uint8 + ComputeUnitPrice fees.ComputeUnitPrice } func (td TxDetails) Empty() bool { @@ -93,15 +94,15 @@ func ParseTx(tx *solanaGo.Transaction, programAddr solanaGo.PublicKey) (TxDetail // The signature at index i corresponds to the public key at index i in message.accountKeys. sender := tx.Message.AccountKeys[0] - // CL node DF transactions should only have a compute budget + ocr2 instruction + // CL node DF transactions should only have a compute unit price + ocr2 instruction if len(tx.Message.Instructions) != 2 { return TxDetails{}, fmt.Errorf("not a node transaction") } - var obsCount uint8 var totalErr error var foundTransmit bool var foundFee bool + txDetails := TxDetails{Sender: sender} for _, instruction := range tx.Message.Instructions { // protect against invalid index if int(instruction.ProgramIDIndex) >= len(tx.Message.AccountKeys) { @@ -113,21 +114,35 @@ func ParseTx(tx *solanaGo.Transaction, programAddr solanaGo.PublicKey) (TxDetail // parse report from tx data (see solana/transmitter.go) start := solana.StoreNonceLen + solana.ReportContextLen end := start + int(solana.ReportLen) + + // handle invalid length + if len(instruction.Data) < (solana.StoreNonceLen + solana.ReportContextLen + int(solana.ReportLen)) { + totalErr = errors.Join(totalErr, fmt.Errorf("transmit: invalid instruction length (%+v)", instruction)) + continue + } + report := types.Report(instruction.Data[start:end]) - count, err := solana.ReportCodec{}.ObserversCountFromReport(report) + var err error + txDetails.ObservationCount, err = solana.ReportCodec{}.ObserversCountFromReport(report) if err != nil { totalErr = errors.Join(totalErr, fmt.Errorf("%w (%+v)", err, instruction)) continue } - obsCount = count foundTransmit = true continue } // find compute budget program instruction if tx.Message.AccountKeys[instruction.ProgramIDIndex] == solanaGo.MustPublicKeyFromBase58(fees.COMPUTE_BUDGET_PROGRAM) { - // future: parsing fee calculation + // parsing compute unit price + var err error + txDetails.ComputeUnitPrice, err = fees.ParseComputeUnitPrice(instruction.Data) + if err != nil { + totalErr = errors.Join(totalErr, fmt.Errorf("computeUnitPrice: %w (%+v)", err, instruction)) + continue + } foundFee = true + continue } } if totalErr != nil { @@ -139,8 +154,5 @@ func ParseTx(tx *solanaGo.Transaction, programAddr solanaGo.PublicKey) (TxDetail return TxDetails{}, fmt.Errorf("unable to parse both Transmit and Fee instructions") } - return TxDetails{ - Sender: sender, - ObservationCount: obsCount, - }, nil + return txDetails, nil } diff --git a/pkg/monitoring/types/txdetails_test.go b/pkg/monitoring/types/txdetails_test.go index e18d482d2..53161e8f0 100644 --- a/pkg/monitoring/types/txdetails_test.go +++ b/pkg/monitoring/types/txdetails_test.go @@ -6,20 +6,29 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( sampleTxResultSigner = solana.MustPublicKeyFromBase58("9YR7YttJFfptQJSo5xrnYoAw1fJyVonC1vxUSqzAgyjY") - - sampleTxResult = rpc.GetTransactionResult{} ) func init() { - if err := json.Unmarshal([]byte(SampleTxResultJSON), &sampleTxResult); err != nil { - panic("unable to unmarshal sampleTxResult") - } +} + +func getTestTxResult(t *testing.T) *rpc.GetTransactionResult { + out := &rpc.GetTransactionResult{} + require.NoError(t, json.Unmarshal([]byte(SampleTxResultJSON), out)) + return out +} + +func getTestTx(t *testing.T) *solana.Transaction { + tx, err := getTestTxResult(t).Transaction.GetTransaction() + require.NoError(t, err) + require.NotNil(t, tx) + return tx } func TestParseTxResult(t *testing.T) { @@ -36,7 +45,7 @@ func TestParseTxResult(t *testing.T) { require.ErrorContains(t, err, "txResult.Transaction") // happy path - res, err := ParseTxResult(&sampleTxResult, SampleTxResultProgram) + res, err := ParseTxResult(getTestTxResult(t), SampleTxResultProgram) require.NoError(t, err) assert.Equal(t, nil, res.Err) @@ -47,40 +56,44 @@ func TestParseTx(t *testing.T) { _, err := ParseTx(nil, SampleTxResultProgram) require.ErrorContains(t, err, "tx is nil") - tx, err := sampleTxResult.Transaction.GetTransaction() - require.NoError(t, err) - require.NotNil(t, tx) - - txMissingSig := *tx // copy + txMissingSig := getTestTx(t) // copy txMissingSig.Signatures = []solana.Signature{} - _, err = ParseTx(&txMissingSig, SampleTxResultProgram) + _, err = ParseTx(txMissingSig, SampleTxResultProgram) require.ErrorContains(t, err, "invalid number of signatures") - txMissingAccounts := *tx // copy + txMissingAccounts := getTestTx(t) // copy txMissingAccounts.Message.AccountKeys = []solana.PublicKey{} - _, err = ParseTx(&txMissingAccounts, SampleTxResultProgram) + _, err = ParseTx(txMissingAccounts, SampleTxResultProgram) require.ErrorContains(t, err, "invalid number of signatures") - prevIndex := tx.Message.Instructions[1].ProgramIDIndex - txInvalidProgramIndex := *tx // copy + txInvalidProgramIndex := getTestTx(t) // copy txInvalidProgramIndex.Message.Instructions[1].ProgramIDIndex = 100 // index 1 is ocr transmit call - out, err := ParseTx(&txInvalidProgramIndex, SampleTxResultProgram) + out, err := ParseTx(txInvalidProgramIndex, SampleTxResultProgram) require.Error(t, err) - tx.Message.Instructions[1].ProgramIDIndex = prevIndex // reset - something shares memory underneath // don't match program - out, err = ParseTx(tx, solana.PublicKey{}) + out, err = ParseTx(getTestTx(t), solana.PublicKey{}) require.Error(t, err) + // invalid length transmit instruction + compute budget instruction + txInvalidTransmitInstruction := getTestTx(t) + txInvalidTransmitInstruction.Message.Instructions[0].Data = []byte{} + txInvalidTransmitInstruction.Message.Instructions[1].Data = []byte{} + _, err = ParseTx(txInvalidTransmitInstruction, SampleTxResultProgram) + require.ErrorContains(t, err, "transmit: invalid instruction length") + + require.ErrorContains(t, err, "computeUnitPrice") + // happy path - out, err = ParseTx(tx, SampleTxResultProgram) + out, err = ParseTx(getTestTx(t), SampleTxResultProgram) require.NoError(t, err) assert.Equal(t, sampleTxResultSigner, out.Sender) assert.Equal(t, uint8(4), out.ObservationCount) + assert.Equal(t, fees.ComputeUnitPrice(0), out.ComputeUnitPrice) // multiple instructions - currently not the case - txMultipleTransmit := *tx - txMultipleTransmit.Message.Instructions = append(tx.Message.Instructions, tx.Message.Instructions[1]) - out, err = ParseTx(&txMultipleTransmit, SampleTxResultProgram) + txMultipleTransmit := getTestTx(t) + txMultipleTransmit.Message.Instructions = append(txMultipleTransmit.Message.Instructions, getTestTx(t).Message.Instructions[1]) + out, err = ParseTx(txMultipleTransmit, SampleTxResultProgram) require.Error(t, err) } diff --git a/pkg/solana/fees/computebudget.go b/pkg/solana/fees/computebudget.go index f0c4a1ec2..ba980f0a2 100644 --- a/pkg/solana/fees/computebudget.go +++ b/pkg/solana/fees/computebudget.go @@ -3,6 +3,7 @@ package fees import ( "bytes" "encoding/binary" + "fmt" "github.com/gagliardetto/solana-go" ) @@ -63,6 +64,19 @@ func (val ComputeUnitPrice) Data() ([]byte, error) { return buf.Bytes(), nil } +func ParseComputeUnitPrice(data []byte) (ComputeUnitPrice, error) { + if len(data) != (1 + 8) { // instruction byte + uint64 + return 0, fmt.Errorf("invalid length: %d", len(data)) + } + + if data[0] != Instruction_SetComputeUnitPrice { + return 0, fmt.Errorf("not SetComputeUnitPrice identifier: %d", data[0]) + } + + // guarantees length 8 + return ComputeUnitPrice(binary.LittleEndian.Uint64(data[1:])), nil +} + // modifies passed in tx to set compute unit price func SetComputeUnitPrice(tx *solana.Transaction, price ComputeUnitPrice) error { // find ComputeBudget program to accounts if it exists diff --git a/pkg/solana/fees/computebudget_test.go b/pkg/solana/fees/computebudget_test.go index b4003281e..4fc3a35c9 100644 --- a/pkg/solana/fees/computebudget_test.go +++ b/pkg/solana/fees/computebudget_test.go @@ -101,3 +101,23 @@ func TestSetComputeUnitPrice(t *testing.T) { }) } + +func TestParseComputeUnitPrice(t *testing.T) { + data, err := ComputeUnitPrice(100).Data() + assert.NoError(t, err) + + v, err := ParseComputeUnitPrice(data) + assert.NoError(t, err) + assert.Equal(t, ComputeUnitPrice(100), v) + + _, err = ParseComputeUnitPrice([]byte{}) + assert.ErrorContains(t, err, "invalid length") + tooLong := [10]byte{} + _, err = ParseComputeUnitPrice(tooLong[:]) + assert.ErrorContains(t, err, "invalid length") + + invalidData := data + invalidData[0] = Instruction_RequestHeapFrame + _, err = ParseComputeUnitPrice(invalidData) + assert.ErrorContains(t, err, "not SetComputeUnitPrice identifier") +}