From 0cb98e5e05c1aaaed34bf9af4d99859b972f78aa Mon Sep 17 00:00:00 2001 From: billettc Date: Mon, 15 Jan 2024 13:57:05 -0500 Subject: [PATCH] wip part of encoding completed --- block/fetcher/error.go | 396 ++++++++++++++++++++++++++++++++++++++ block/fetcher/rpc.go | 49 ++--- block/fetcher/rpc_test.go | 42 ++++ 3 files changed, 452 insertions(+), 35 deletions(-) create mode 100644 block/fetcher/error.go diff --git a/block/fetcher/error.go b/block/fetcher/error.go new file mode 100644 index 00000000..ff08700a --- /dev/null +++ b/block/fetcher/error.go @@ -0,0 +1,396 @@ +package fetcher + +import ( + "encoding/binary" + "fmt" + + bin "github.com/streamingfast/binary" +) + +type errDetail interface { + Encode(encoder *bin.Encoder) error +} + +type TrxErrCode int32 + +const ( + TrxErr_AccountInUse TrxErrCode = iota + TrxErr_AccountLoadedTwice + TrxErr_AccountNotFound + TrxErr_ProgramAccountNotFound + TrxErr_InsufficientFundsForFee + TrxErr_InvalidAccountForFee + TrxErr_AlreadyProcessed + TrxErr_BlockhashNotFound + TrxErr_InstructionError + TrxErr_CallChainTooDeep + TrxErr_MissingSignatureForFee + TrxErr_InvalidAccountIndex + TrxErr_SignatureFailure + TrxErr_InvalidProgramForExecution + TrxErr_SanitizeFailure + TrxErr_ClusterMaintenance + TrxErr_AccountBorrowOutstanding + TrxErr_WouldExceedMaxBlockCostLimit + TrxErr_UnsupportedVersion + TrxErr_InvalidWritableAccount + TrxErr_WouldExceedMaxAccountCostLimit + TrxErr_WouldExceedAccountDataBlockLimit + TrxErr_TooManyAccountLocks + TrxErr_AddressLookupTableNotFound + TrxErr_InvalidAddressLookupTableOwner + TrxErr_InvalidAddressLookupTableData + TrxErr_InvalidAddressLookupTableIndex + TrxErr_InvalidRentPayingAccount + TrxErr_WouldExceedMaxVoteCostLimit + TrxErr_WouldExceedAccountDataTotalLimit + TrxErr_DuplicateInstruction + TrxErr_InsufficientFundsForRent + TrxErr_MaxLoadedAccountsDataSizeExceeded + TrxErr_InvalidLoadedAccountsDataSizeLimit + TrxErr_ResanitizationNeeded + TrxErr_ProgramExecutionTemporarilyRestricted + TrxErr_UnbalancedTransaction +) + +var trxErrorMap = map[string]TrxErrCode{ + "AccountInUse": TrxErr_AccountInUse, + "AccountLoadedTwice": TrxErr_AccountLoadedTwice, + "AccountNotFound": TrxErr_AccountNotFound, + "ProgramAccountNotFound": TrxErr_ProgramAccountNotFound, + "InsufficientFundsForFee": TrxErr_InsufficientFundsForFee, + "InvalidAccountForFee": TrxErr_InvalidAccountForFee, + "AlreadyProcessed": TrxErr_AlreadyProcessed, + "BlockhashNotFound": TrxErr_BlockhashNotFound, + "InstructionError": TrxErr_InstructionError, + "CallChainTooDeep": TrxErr_CallChainTooDeep, + "MissingSignatureForFee": TrxErr_MissingSignatureForFee, + "InvalidAccountIndex": TrxErr_InvalidAccountIndex, + "SignatureFailure": TrxErr_SignatureFailure, + "InvalidProgramForExecution": TrxErr_InvalidProgramForExecution, + "SanitizeFailure": TrxErr_SanitizeFailure, + "ClusterMaintenance": TrxErr_ClusterMaintenance, + "AccountBorrowOutstanding": TrxErr_AccountBorrowOutstanding, + "WouldExceedMaxBlockCostLimit": TrxErr_WouldExceedMaxBlockCostLimit, + "UnsupportedVersion": TrxErr_UnsupportedVersion, + "InvalidWritableAccount": TrxErr_InvalidWritableAccount, + "WouldExceedMaxAccountCostLimit": TrxErr_WouldExceedMaxAccountCostLimit, + "WouldExceedAccountDataBlockLimit": TrxErr_WouldExceedAccountDataBlockLimit, + "TooManyAccountLocks": TrxErr_TooManyAccountLocks, + "AddressLookupTableNotFound": TrxErr_AddressLookupTableNotFound, + "InvalidAddressLookupTableOwner": TrxErr_InvalidAddressLookupTableOwner, + "InvalidAddressLookupTableData": TrxErr_InvalidAddressLookupTableData, + "InvalidAddressLookupTableIndex": TrxErr_InvalidAddressLookupTableIndex, + "InvalidRentPayingAccount": TrxErr_InvalidRentPayingAccount, + "WouldExceedMaxVoteCostLimit": TrxErr_WouldExceedMaxVoteCostLimit, + "WouldExceedAccountDataTotalLimit": TrxErr_WouldExceedAccountDataTotalLimit, + "DuplicateInstruction": TrxErr_DuplicateInstruction, + "InsufficientFundsForRent": TrxErr_InsufficientFundsForRent, + "MaxLoadedAccountsDataSizeExceeded": TrxErr_MaxLoadedAccountsDataSizeExceeded, + "InvalidLoadedAccountsDataSizeLimit": TrxErr_InvalidLoadedAccountsDataSizeLimit, + "ResanitizationNeeded": TrxErr_ResanitizationNeeded, + "ProgramExecutionTemporarilyRestricted": TrxErr_ProgramExecutionTemporarilyRestricted, + "UnbalancedTransaction": TrxErr_UnbalancedTransaction, +} + +type TransactionError struct { + TrxErrCode + detail errDetail +} + +func MustNewTransactionError(e any) *TransactionError { + if e == nil { + return nil + } + + if errorName, ok := e.(string); ok { + if errorCode, ok := trxErrorMap[errorName]; ok { + return &TransactionError{errorCode, nil} + } + panic(fmt.Errorf("unknown error name: %s", errorName)) + } + + if mapErr, ok := e.(map[string]interface{}); ok { + if len(mapErr) != 1 { + panic(fmt.Errorf("unknown error map: %v", mapErr)) + } + for errorName, detailMap := range mapErr { + if errorCode, ok := trxErrorMap[errorName]; ok { + var errorDetail errDetail + + // //8 0 0 0 3 25 0 0 0 113 23 0 0 + // //"err":{"InstructionError":[3,{"Custom":22}]} + // + // //[1 0 0 0] + // //"err":"AccountInUse" + // + // //8 0 0 0 -> TransactionError.InstructionError + // //3 -> instruction index + // //25 0 0 0 -> InstructionError.Custom + // //113 23 0 0 -> u32 error code + + switch errorCode { + case TrxErr_InstructionError: + errorDetail = MustNewInstructionError(detailMap) + case TrxErr_DuplicateInstruction: + //todo: DuplicateInstruction(u8) + case TrxErr_InsufficientFundsForRent: + //todo: InsufficientFundsForRent { account_index: u8 + case TrxErr_ProgramExecutionTemporarilyRestricted: + //todo: ProgramExecutionTemporarilyRestricted { account_index: u8 } + default: + panic(fmt.Errorf("unknown error code: %d", errorCode)) + } + + return &TransactionError{errorCode, errorDetail} + } + panic(fmt.Errorf("unknown error name: %s", errorName)) + } + //we should never get here since we checked the length of the map and we are exiting only one element + } + + panic(fmt.Errorf("unknown error type: %T", e)) +} + +func (e *TransactionError) Encode(encoder *bin.Encoder) error { + err := encoder.WriteUint32(uint32(e.TrxErrCode), binary.LittleEndian) + if err != nil { + return err + } + if e.detail == nil { + return nil + } + err = e.detail.Encode(encoder) + if err != nil { + return fmt.Errorf("unable to encode error detail: %w", err) + } + return nil +} + +type InstructionErrorCode uint32 + +const ( + InstructionError_GenericError InstructionErrorCode = iota + InstructionError_InvalidArgument + InstructionError_InvalidInstructionData + InstructionError_InvalidAccountData + InstructionError_AccountDataTooSmall + InstructionError_InsufficientFunds + InstructionError_IncorrectProgramId + InstructionError_MissingRequiredSignature + InstructionError_AccountAlreadyInitialized + InstructionError_UninitializedAccount + InstructionError_UnbalancedInstruction + InstructionError_ModifiedProgramId + InstructionError_ExternalAccountLamportSpend + InstructionError_ExternalAccountDataModified + InstructionError_ReadonlyLamportChange + InstructionError_ReadonlyDataModified + InstructionError_DuplicateAccountIndex + InstructionError_ExecutableModified + InstructionError_RentEpochModified + InstructionError_NotEnoughAccountKeys + InstructionError_AccountDataSizeChanged + InstructionError_AccountNotExecutable + InstructionError_AccountBorrowFailed + InstructionError_AccountBorrowOutstanding + InstructionError_DuplicateAccountOutOfSync + InstructionError_Custom + InstructionError_InvalidError + InstructionError_ExecutableDataModified + InstructionError_ExecutableLamportChange + InstructionError_ExecutableAccountNotRentExempt + InstructionError_UnsupportedProgramId + InstructionError_CallDepth + InstructionError_MissingAccount + InstructionError_ReentrancyNotAllowed + InstructionError_MaxSeedLengthExceeded + InstructionError_InvalidSeeds + InstructionError_InvalidRealloc + InstructionError_ComputationalBudgetExceeded + InstructionError_PrivilegeEscalation + InstructionError_ProgramEnvironmentSetupFailure + InstructionError_ProgramFailedToComplete + InstructionError_ProgramFailedToCompile + InstructionError_Immutable + InstructionError_IncorrectAuthority + InstructionError_BorshIoError + InstructionError_AccountNotRentExempt + InstructionError_InvalidAccountOwner + InstructionError_ArithmeticOverflow + InstructionError_UnsupportedSysvar + InstructionError_IllegalOwner + InstructionError_MaxAccountsDataAllocationsExceeded + InstructionError_MaxAccountsExceeded + InstructionError_MaxInstructionTraceLengthExceeded + InstructionError_BuiltinProgramsMustConsumeComputeUnits +) + +var instructionErrorMap = map[string]InstructionErrorCode{ + "GenericError": InstructionError_GenericError, + "InvalidArgument": InstructionError_InvalidArgument, + "InvalidInstructionData": InstructionError_InvalidInstructionData, + "InvalidAccountData": InstructionError_InvalidAccountData, + "AccountDataTooSmall": InstructionError_AccountDataTooSmall, + "InsufficientFunds": InstructionError_InsufficientFunds, + "IncorrectProgramId": InstructionError_IncorrectProgramId, + "MissingRequiredSignature": InstructionError_MissingRequiredSignature, + "AccountAlreadyInitialized": InstructionError_AccountAlreadyInitialized, + "UninitializedAccount": InstructionError_UninitializedAccount, + "UnbalancedInstruction": InstructionError_UnbalancedInstruction, + "ModifiedProgramId": InstructionError_ModifiedProgramId, + "ExternalAccountLamportSpend": InstructionError_ExternalAccountLamportSpend, + "ExternalAccountDataModified": InstructionError_ExternalAccountDataModified, + "ReadonlyLamportChange": InstructionError_ReadonlyLamportChange, + "ReadonlyDataModified": InstructionError_ReadonlyDataModified, + "DuplicateAccountIndex": InstructionError_DuplicateAccountIndex, + "ExecutableModified": InstructionError_ExecutableModified, + "RentEpochModified": InstructionError_RentEpochModified, + "NotEnoughAccountKeys": InstructionError_NotEnoughAccountKeys, + "AccountDataSizeChanged": InstructionError_AccountDataSizeChanged, + "AccountNotExecutable": InstructionError_AccountNotExecutable, + "AccountBorrowFailed": InstructionError_AccountBorrowFailed, + "AccountBorrowOutstanding": InstructionError_AccountBorrowOutstanding, + "DuplicateAccountOutOfSync": InstructionError_DuplicateAccountOutOfSync, + "Custom": InstructionError_Custom, + "InvalidError": InstructionError_InvalidError, + "ExecutableDataModified": InstructionError_ExecutableDataModified, + "ExecutableLamportChange": InstructionError_ExecutableLamportChange, + "ExecutableAccountNotRentExempt": InstructionError_ExecutableAccountNotRentExempt, + "UnsupportedProgramId": InstructionError_UnsupportedProgramId, + "CallDepth": InstructionError_CallDepth, + "MissingAccount": InstructionError_MissingAccount, + "ReentrancyNotAllowed": InstructionError_ReentrancyNotAllowed, + "MaxSeedLengthExceeded": InstructionError_MaxSeedLengthExceeded, + "InvalidSeeds": InstructionError_InvalidSeeds, + "InvalidRealloc": InstructionError_InvalidRealloc, + "ComputationalBudgetExceeded": InstructionError_ComputationalBudgetExceeded, + "PrivilegeEscalation": InstructionError_PrivilegeEscalation, + "ProgramEnvironmentSetupFailure": InstructionError_ProgramEnvironmentSetupFailure, + "ProgramFailedToComplete": InstructionError_ProgramFailedToComplete, + "ProgramFailedToCompile": InstructionError_ProgramFailedToCompile, + "Immutable": InstructionError_Immutable, + "IncorrectAuthority": InstructionError_IncorrectAuthority, + "BorshIoError": InstructionError_BorshIoError, + "AccountNotRentExempt": InstructionError_AccountNotRentExempt, + "InvalidAccountOwner": InstructionError_InvalidAccountOwner, + "ArithmeticOverflow": InstructionError_ArithmeticOverflow, + "UnsupportedSysvar": InstructionError_UnsupportedSysvar, + "IllegalOwner": InstructionError_IllegalOwner, + "MaxAccountsDataAllocationsExceeded": InstructionError_MaxAccountsDataAllocationsExceeded, + "MaxAccountsExceeded": InstructionError_MaxAccountsExceeded, + "MaxInstructionTraceLengthExceeded": InstructionError_MaxInstructionTraceLengthExceeded, + "BuiltinProgramsMustConsumeComputeUnits": InstructionError_BuiltinProgramsMustConsumeComputeUnits, +} + +type InstructionError struct { + InstructionErrorCode + InstructionIndex byte + detail errDetail +} + +func MustNewInstructionError(e any) *InstructionError { + if e == nil { + return nil + } + + parts := e.([]any) + if len(parts) != 2 { + panic(fmt.Errorf("invalid number of parts for InstructionError: %d", len(parts))) + } + + instructionIndex := byte(parts[0].(float64)) + + if errorName, isString := parts[1].(string); isString { + if errorCode, ok := instructionErrorMap[errorName]; ok { + return &InstructionError{InstructionErrorCode: errorCode, InstructionIndex: instructionIndex} + } + panic(fmt.Errorf("unknown error name: %s", errorName)) + } + + if mapErr, ok := parts[1].(map[string]any); ok { + if len(mapErr) != 1 { + panic(fmt.Errorf("unknown error map: %v", mapErr)) + } + for errorName, details := range mapErr { + if errorCode, ok := instructionErrorMap[errorName]; ok { + var errorDetail errDetail + + switch errorCode { + case InstructionError_Custom: + errorDetail = MustNewInstructionCustomError(details) + case InstructionError_BorshIoError: + errorDetail = MustNewBorshIoError(details) + default: + panic(fmt.Errorf("unknown error code: %d", errorCode)) + } + + return &InstructionError{InstructionErrorCode: errorCode, InstructionIndex: instructionIndex, detail: errorDetail} + } + panic(fmt.Errorf("unknown error name: %s", errorName)) + } + //we should never get here since we checked the length of the map and we are exiting only one element + } + + panic(fmt.Errorf("unknown error type: %T", e)) + +} + +func (i *InstructionError) Encode(encoder *bin.Encoder) error { + err := encoder.WriteByte(i.InstructionIndex) + if err != nil { + return fmt.Errorf("unable to encode instruction index: %w", err) + } + err = encoder.WriteUint32(uint32(i.InstructionErrorCode), binary.LittleEndian) + if err != nil { + return fmt.Errorf("unable to encode error code: %w", err) + } + if i.detail == nil { + return nil + } + err = i.detail.Encode(encoder) + if err != nil { + return fmt.Errorf("unable to encode error detail: %w", err) + } + return nil +} + +type InstructionCustomError struct { + CustomErrorCode uint32 +} + +func MustNewInstructionCustomError(e any) InstructionCustomError { + customErrorCode, ok := e.(float64) + if !ok { + panic(fmt.Errorf("expected float64, got: %T", e)) + } + + return InstructionCustomError{ + CustomErrorCode: uint32(customErrorCode), + } +} + +func (i InstructionCustomError) Encode(encoder *bin.Encoder) error { + err := encoder.WriteUint32(i.CustomErrorCode, binary.LittleEndian) + if err != nil { + return fmt.Errorf("unable to encode custom error code: %w", err) + } + return nil +} + +type BorshIoError struct { + msg string +} + +func MustNewBorshIoError(a any) BorshIoError { + msg, ok := a.(string) + if !ok { + panic(fmt.Errorf("expected string, got: %T", a)) + } + return BorshIoError{msg: msg} +} + +func (b BorshIoError) Encode(encoder *bin.Encoder) error { + //TODO implement me + panic("implement me") +} diff --git a/block/fetcher/rpc.go b/block/fetcher/rpc.go index 69b32bdc..87c386c8 100644 --- a/block/fetcher/rpc.go +++ b/block/fetcher/rpc.go @@ -1,6 +1,7 @@ package fetcher import ( + "bytes" "context" "encoding/base64" "encoding/json" @@ -8,6 +9,8 @@ import ( "math" "time" + bin "github.com/streamingfast/binary" + "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" pbbstream "github.com/streamingfast/bstream/pb/sf/bstream/v1" @@ -306,45 +309,21 @@ func compileInstructionsToPbInnerInstructionArray(instructions []solana.Compiled return } -type TransactionError struct { - Type string `json:"err"` - Details string -} - -func toPbTransactionError(err interface{}) (*pbsol.TransactionError, error) { - if err == nil { +func toPbTransactionError(e interface{}) (*pbsol.TransactionError, error) { + if e == nil { return nil, nil } - if mapErr, ok := err.(map[string]interface{}); ok { - for key, value := range mapErr { - detail, err := json.Marshal(value) - if err != nil { - return nil, fmt.Errorf("decoding transaction error: %w", err) - } - trxErr := &TransactionError{ - Type: key, - Details: string(detail), - } - fmt.Println(trxErr) - return nil, nil - } + txErr := MustNewTransactionError(e) + buf := bytes.NewBuffer(nil) + encoder := bin.NewEncoder(buf) + err := txErr.Encode(encoder) + if err != nil { + return nil, err } - - //8 0 0 0 3 25 0 0 0 113 23 0 0 - - // "InstructionError": [ - // 3, - // { - // "Custom": 6001 - // } - // ] - - //8 0 0 0 -> TransactionError.InstructionError - //3 -> instruction index - //25 0 0 0 -> InstructionError.Custom - //113 23 0 0 -> u32 error code - panic("not implemented") //todo : implement when test with a failed transaction + return &pbsol.TransactionError{ + Err: buf.Bytes(), + }, nil } func toPbTransaction(transaction *solana.Transaction) *pbsol.Transaction { diff --git a/block/fetcher/rpc_test.go b/block/fetcher/rpc_test.go index 53e034b5..ac5ebd32 100644 --- a/block/fetcher/rpc_test.go +++ b/block/fetcher/rpc_test.go @@ -1,11 +1,13 @@ package fetcher import ( + "bytes" "encoding/json" "os" "testing" "github.com/gagliardetto/solana-go/rpc" + bin "github.com/streamingfast/binary" "github.com/test-go/testify/require" ) @@ -28,3 +30,43 @@ func Test_ToPBTransaction(t *testing.T) { // } //} } + +func Test_InstructionEncode(t *testing.T) { + cases := []struct { + name string + instruction *InstructionError + expected []byte + }{ + { + name: "sunny path", + instruction: &InstructionError{ + InstructionErrorCode: 0, + InstructionIndex: 1, + detail: nil, + }, + expected: []byte{1, byte(InstructionError_GenericError), 0, 0, 0}, + }, + { + name: "custom", + instruction: &InstructionError{ + InstructionErrorCode: 25, + InstructionIndex: 9, + detail: InstructionCustomError{ + CustomErrorCode: 6001, + }, + }, + expected: []byte{9, byte(InstructionError_Custom), 0, 0, 0, 113, 23, 0, 0}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + encoder := bin.NewEncoder(buf) + err := c.instruction.Encode(encoder) + require.NoError(t, err) + require.Equal(t, c.expected, buf.Bytes()) + + }) + } +}