Skip to content

Commit

Permalink
parse compute unit price from tx details
Browse files Browse the repository at this point in the history
  • Loading branch information
aalu1418 committed Apr 19, 2024
1 parent 811bed3 commit 30f9ac5
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 33 deletions.
32 changes: 22 additions & 10 deletions pkg/monitoring/types/txdetails.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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
}
59 changes: 36 additions & 23 deletions pkg/monitoring/types/txdetails_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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)
}
14 changes: 14 additions & 0 deletions pkg/solana/fees/computebudget.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fees
import (
"bytes"
"encoding/binary"
"fmt"

"github.com/gagliardetto/solana-go"
)
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions pkg/solana/fees/computebudget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

0 comments on commit 30f9ac5

Please sign in to comment.