From 6aed66ecfbefe72ef9487fa8d7f20d50e30405d7 Mon Sep 17 00:00:00 2001 From: cavemanloverboy <93507302+cavemanloverboy@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:10:45 +0000 Subject: [PATCH] blockprod: include failed transactions the protocol treats as processed --- pkg/blockprod/bank.go | 2 +- pkg/blockprod/bank_failed_test.go | 143 +++++++++++++++++++++++++ pkg/blockprod/bank_test.go | 19 +++- pkg/replay/commit.go | 47 ++++++-- pkg/replay/commit_test.go | 14 +-- pkg/replay/includable_failure_test.go | 148 ++++++++++++++++++++++++++ 6 files changed, 352 insertions(+), 21 deletions(-) create mode 100644 pkg/blockprod/bank_failed_test.go create mode 100644 pkg/replay/includable_failure_test.go diff --git a/pkg/blockprod/bank.go b/pkg/blockprod/bank.go index 8535e86ec..47d1e4c00 100644 --- a/pkg/blockprod/bank.go +++ b/pkg/blockprod/bank.go @@ -292,7 +292,7 @@ func (b *WorkingBank) forgeTransaction(tx *solana.Transaction, wireSize int, pre Arena: b.borrowedAccounts, }, prepared) if output.ProcessingResult.TransactionError != nil { - feeInfo, err := replay.ApplyFeesOnlyTransaction(b.slotCtx, tx, output) + feeInfo, err := replay.ApplyFailedTransaction(b.slotCtx, tx, output) if err != nil { return ForgeDroppedExecution, costmodel.ExceedNone } diff --git a/pkg/blockprod/bank_failed_test.go b/pkg/blockprod/bank_failed_test.go new file mode 100644 index 000000000..0fd33f813 --- /dev/null +++ b/pkg/blockprod/bank_failed_test.go @@ -0,0 +1,143 @@ +package blockprod + +import ( + "sync" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/costmodel" + "github.com/Overclock-Validator/mithril/pkg/replay" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" + "github.com/gagliardetto/solana-go" + computebudget "github.com/gagliardetto/solana-go/programs/compute-budget" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func bankLamports(t *testing.T, slotCtx *sealevel.SlotCtx, pubkey solana.PublicKey) uint64 { + t.Helper() + acct, err := slotCtx.GetAccount(pubkey) + if err != nil { + return 0 + } + return acct.Lamports +} + +func TestWorkingBankIncludesExecutionFailureAsFailed(t *testing.T) { + env := NewTestEnv(TestEnvConfig{}) + defer env.Close() + // The fee is payable, but the payer holds 10 SOL and cannot transfer 20. + tx := mustSignBankTestTransaction(t, + system.NewTransferInstruction(20_000_000_000, txfixture.PayerPubkey(), txfixture.DestPubkey()).Build()) + wire, err := tx.MarshalBinary() + require.NoError(t, err) + payerBefore := bankLamports(t, env.SlotCtx, txfixture.PayerPubkey()) + destBefore := bankLamports(t, env.SlotCtx, txfixture.DestPubkey()) + + result, reason := env.Bank.ForgeTransaction(tx, len(wire)) + require.Equal(t, ForgeAccepted, result) + require.Equal(t, costmodel.ExceedNone, reason) + assert.Equal(t, payerBefore-5_000, bankLamports(t, env.SlotCtx, txfixture.PayerPubkey()), "only the fee is charged") + assert.Equal(t, destBefore, bankLamports(t, env.SlotCtx, txfixture.DestPubkey())) + assert.Equal(t, uint64(5_000), env.Bank.TxFeeAccumulator().TotalFees) + assert.Equal(t, []*solana.Transaction{tx}, env.Bank.ForgedTransactions()) + assert.Equal(t, uint64(1), env.Bank.NumSignatures()) + assert.Equal(t, 1, env.Bank.EntryBuilder().PendingCount()) + assert.NotZero(t, env.Bank.CostTracker().BlockCost()) + + result, _ = env.Bank.ForgeTransaction(tx, len(wire)) + assert.Equal(t, ForgeDroppedAlreadyProcessed, result, "the failed status is recorded") + assert.Equal(t, payerBefore-5_000, bankLamports(t, env.SlotCtx, txfixture.PayerPubkey())) +} + +func TestWorkingBankDropsFailuresBeforeExecution(t *testing.T) { + payerKey := txfixture.PayerPrivateKey() + sign := func(t *testing.T, blockhash solana.Hash, instructions ...solana.Instruction) *solana.Transaction { + tx, err := solana.NewTransaction(instructions, blockhash, solana.TransactionPayer(txfixture.PayerPubkey())) + require.NoError(t, err) + _, err = tx.Sign(func(solana.PublicKey) *solana.PrivateKey { return &payerKey }) + require.NoError(t, err) + return tx + } + transfer := system.NewTransferInstruction(1, txfixture.PayerPubkey(), txfixture.DestPubkey()).Build() + cases := map[string]*solana.Transaction{ + "compute-budget instruction error": sign(t, txfixture.TestBlockhash(), + solana.NewInstruction(computebudget.ProgramID, nil, []byte{0xFF}), transfer), + "unknown blockhash": sign(t, solana.Hash{0x42}, transfer), + } + for name, tx := range cases { + t.Run(name, func(t *testing.T) { + env := NewTestEnv(TestEnvConfig{}) + defer env.Close() + payerBefore := bankLamports(t, env.SlotCtx, txfixture.PayerPubkey()) + + result, _ := env.Bank.ForgeTransaction(tx, 0) + require.Equal(t, ForgeDroppedExecution, result) + assert.Equal(t, payerBefore, bankLamports(t, env.SlotCtx, txfixture.PayerPubkey())) + assert.Empty(t, env.Bank.ForgedTransactions()) + assert.Zero(t, env.Bank.TxFeeAccumulator().TotalFees) + assert.Zero(t, env.Bank.EntryBuilder().PendingCount()) + }) + } +} + +// TestLeaderFailedTransactionsMatchReplay forges each transaction on a leader +// bank and processes it with replay from the same state: replay must accept +// exactly what the leader includes, with the same balances and fee. +func TestLeaderFailedTransactionsMatchReplay(t *testing.T) { + rentSysvar := sealevel.NewDefaultRentSysvar() + rentMin := rentSysvar.MinimumBalance(0) + newAccount := solana.PublicKey{0xEE} + transfer := func(lamports uint64, to solana.PublicKey) solana.Instruction { + return system.NewTransferInstruction(lamports, txfixture.PayerPubkey(), to).Build() + } + cases := []struct { + name string + payer uint64 + tx func(t *testing.T) *solana.Transaction + included bool + }{ + {name: "success", tx: func(t *testing.T) *solana.Transaction { return mustSignBankTestTransaction(t, transfer(1, txfixture.DestPubkey())) }, included: true}, + {name: "instruction error", tx: func(t *testing.T) *solana.Transaction { + return mustSignBankTestTransaction(t, transfer(20_000_000_000, txfixture.DestPubkey())) + }, included: true}, + {name: "new account below rent", tx: func(t *testing.T) *solana.Transaction { return mustSignBankTestTransaction(t, transfer(1, newAccount)) }, included: true}, + {name: "payer left below rent", payer: 5_000 + 1_000_000 + 100, tx: func(t *testing.T) *solana.Transaction { + return mustSignBankTestTransaction(t, transfer(1_000_000, txfixture.DestPubkey())) + }, included: true}, + {name: "fees-only load failure", tx: func(t *testing.T) *solana.Transaction { + return mustSignBankTestTransaction(t, computebudget.NewSetLoadedAccountsDataSizeLimitInstruction(1).Build(), transfer(1, txfixture.DestPubkey())) + }, included: true}, + {name: "fee-payer rent", payer: rentMin + 5_000 - 1, tx: func(t *testing.T) *solana.Transaction { + return mustSignBankTestTransaction(t, transfer(1, txfixture.DestPubkey())) + }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tx := tc.tx(t) + leader := NewTestEnv(TestEnvConfig{}) + defer leader.Close() + if tc.payer != 0 { + setPayerLamports(t, leader, tc.payer) + } + result, _ := leader.Bank.ForgeTransaction(tx, 0) + require.Equal(t, tc.included, result == ForgeAccepted, "leader result %s", result) + + validator := NewTestEnv(TestEnvConfig{}) + defer validator.Close() + if tc.payer != 0 { + setPayerLamports(t, validator, tc.payer) + } + feeInfo, _, _ := replay.ProcessTransaction(validator.SlotCtx, &sync.WaitGroup{}, tx, nil, nil, nil, false) + require.Equal(t, tc.included, feeInfo != nil, "replay must accept exactly what the leader includes") + if !tc.included { + return + } + assert.Equal(t, feeInfo.TotalFee, leader.Bank.TxFeeAccumulator().TotalFees) + for _, key := range []solana.PublicKey{txfixture.PayerPubkey(), txfixture.DestPubkey(), newAccount} { + assert.Equal(t, bankLamports(t, validator.SlotCtx, key), bankLamports(t, leader.SlotCtx, key), "%s", key) + } + }) + } +} diff --git a/pkg/blockprod/bank_test.go b/pkg/blockprod/bank_test.go index 13e13029f..b2f4781f6 100644 --- a/pkg/blockprod/bank_test.go +++ b/pkg/blockprod/bank_test.go @@ -265,7 +265,7 @@ func TestWorkingBankAcceptsTransferThatDrainsPayerToZero(t *testing.T) { assert.Equal(t, destBefore.Lamports+transfer, destAfter.Lamports) } -func TestWorkingBankDropsTransferThatLeavesPayerBelowRent(t *testing.T) { +func TestWorkingBankIncludesTransferThatLeavesPayerBelowRentAsFailed(t *testing.T) { env := NewTestEnv(TestEnvConfig{}) defer env.Close() @@ -276,14 +276,27 @@ func TestWorkingBankDropsTransferThatLeavesPayerBelowRent(t *testing.T) { destBefore, err := env.SlotCtx.GetAccount(txfixture.DestPubkey()) require.NoError(t, err) tx := mustSignedTransfer(t, transfer) + // The payer can pay the fee and stay rent exempt, so it passes fee-payer + // validation; only the transfer leaves it below rent, which the + // post-execution rent-state check reports. That failure is includable. + preview := replay.LoadAndExecuteTransaction(replay.LoadAndExecuteTransactionInput{ + SlotCtx: env.SlotCtx, Transaction: tx, LeanResult: true, + }) + require.NotNil(t, preview.ProcessingResult.TransactionError) + require.Equal(t, replay.TransactionErrorInsufficientFundsForRent, preview.ProcessingResult.TransactionError.ErrorType) + require.NotNil(t, preview.ExecCtx) + result, reason := env.Bank.ForgeTransaction(tx, 200) - require.Equal(t, ForgeDroppedExecution, result) + require.Equal(t, ForgeAccepted, result) require.Equal(t, costmodel.ExceedNone, reason) destAfter, err := env.SlotCtx.GetAccount(txfixture.DestPubkey()) require.NoError(t, err) assert.Equal(t, destBefore.Lamports, destAfter.Lamports) - assert.Empty(t, env.Bank.ForgedTransactions()) + payerAfter, err := env.SlotCtx.GetAccount(txfixture.PayerPubkey()) + require.NoError(t, err) + assert.Equal(t, transfer+leftover, payerAfter.Lamports, "only the fee is charged") + assert.Equal(t, []*solana.Transaction{tx}, env.Bank.ForgedTransactions()) } func TestWorkingBankForgesTransfer(t *testing.T) { diff --git a/pkg/replay/commit.go b/pkg/replay/commit.go index 67b84647f..63a5c9f10 100644 --- a/pkg/replay/commit.go +++ b/pkg/replay/commit.go @@ -1,12 +1,14 @@ package replay import ( + "errors" "fmt" "sync/atomic" "time" "github.com/Overclock-Validator/mithril/pkg/fees" "github.com/Overclock-Validator/mithril/pkg/metrics" + "github.com/Overclock-Validator/mithril/pkg/rent" "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/gagliardetto/solana-go" ) @@ -67,26 +69,51 @@ func ApplySuccessfulTransaction(slotCtx *sealevel.SlotCtx, output LoadAndExecute return applySuccessfulTransactionState(slotCtx, output.ExecCtx, output.ExecutionResult) } -// ApplyFeesOnlyTransaction commits the fee-payer and durable-nonce rollback -// state for an account-load failure that Agave treats as processable. The -// transaction itself remains failed, but must be recorded in the block. -func ApplyFeesOnlyTransaction(slotCtx *sealevel.SlotCtx, tx *solana.Transaction, output LoadAndExecuteTransactionOutput) (*fees.TxFeeInfo, error) { +// includableFailure reports whether a failed transaction was processed, so a +// leader may record it in the block as failed with only its fee charged: it +// failed loading accounts after its fee payer was validated (fees-only), or +// during execution, including the post-execution rent-state check. The same +// error types raised before execution are not includable, nor are no-ops. +func includableFailure(output LoadAndExecuteTransactionOutput) bool { txErr := output.ProcessingResult.TransactionError - if txErr == nil { - return nil, fmt.Errorf("fees-only transaction has no transaction error") + if txErr == nil || output.ProcessedAsNoOp { + return false } switch txErr.ErrorType { case TransactionErrorMaxLoadedAccountsDataSizeExceeded, TransactionErrorInvalidProgramForExecution, TransactionErrorProgramAccountNotFound: - default: - return nil, fmt.Errorf("transaction error %s is not a processable fees-only load failure", txErr.ErrorType.String()) + return true + case TransactionErrorInstructionError: + // Compute-budget instructions are parsed before execution, which is + // the only stage that creates an execution context. + return output.ExecCtx != nil + case TransactionErrorInsufficientFundsForRent: + // The fee-payer rent check before execution reports a plain error; only + // the post-execution check reports a rent-state transition. + var transition *rent.RentStateTransitionError + return errors.As(txErr.InstructionError, &transition) + } + return false +} + +// ApplyFailedTransaction commits the fee-payer and durable-nonce rollback +// state of a failed transaction that includableFailure accepts. The +// transaction itself remains failed, but must be recorded in the block. Any +// other failure commits nothing. +func ApplyFailedTransaction(slotCtx *sealevel.SlotCtx, tx *solana.Transaction, output LoadAndExecuteTransactionOutput) (*fees.TxFeeInfo, error) { + txErr := output.ProcessingResult.TransactionError + if txErr == nil { + return nil, fmt.Errorf("failed transaction has no transaction error") + } + if !includableFailure(output) { + return nil, fmt.Errorf("transaction error %s is not an includable failure", txErr.ErrorType.String()) } if slotCtx == nil || tx == nil || output.ComputeBudgetLimits == nil || output.FeeInfo == nil { - return nil, fmt.Errorf("fees-only transaction is missing required processing state") + return nil, fmt.Errorf("failed transaction is missing required processing state") } - // The load error is the transaction's recorded status, not a failure to + // The failure is the transaction's recorded status, not a failure to // publish its fee/nonce effects, so do not return it as an apply error. return handleFailedTx(slotCtx, tx, output.Instrs, output.ComputeBudgetLimits, nil, nil) } diff --git a/pkg/replay/commit_test.go b/pkg/replay/commit_test.go index c18631682..14c3dd4b3 100644 --- a/pkg/replay/commit_test.go +++ b/pkg/replay/commit_test.go @@ -102,7 +102,7 @@ func TestApplySuccessfulTransactionCommitsV1TransferWithInlineBudget(t *testing. assert.Equal(t, destBefore.Lamports+8, destAfter.Lamports) } -func TestApplyFeesOnlyTransactionCommitsProcessableLoadFailures(t *testing.T) { +func TestApplyFailedTransactionCommitsProcessableLoadFailures(t *testing.T) { for _, errType := range []TransactionErrorType{ TransactionErrorMaxLoadedAccountsDataSizeExceeded, TransactionErrorInvalidProgramForExecution, @@ -127,7 +127,7 @@ func TestApplyFeesOnlyTransactionCommitsProcessableLoadFailures(t *testing.T) { }, } - feeInfo, err := ApplyFeesOnlyTransaction(slotCtx, tx, output) + feeInfo, err := ApplyFailedTransaction(slotCtx, tx, output) require.NoError(t, err) require.NotNil(t, feeInfo) assert.Equal(t, uint64(5_000), feeInfo.TotalFee) @@ -138,7 +138,7 @@ func TestApplyFeesOnlyTransactionCommitsProcessableLoadFailures(t *testing.T) { } } -func TestApplyFeesOnlyTransactionRejectsUnprocessableError(t *testing.T) { +func TestApplyFailedTransactionRejectsUnprocessableError(t *testing.T) { slotCtx, cleanup := newCommitTestSlotCtx() defer cleanup() tx, err := solana.TransactionFromBytes(txfixture.MustSignedTransferWire(0)) @@ -153,15 +153,15 @@ func TestApplyFeesOnlyTransactionRejectsUnprocessableError(t *testing.T) { ComputeBudgetLimits: &sealevel.ComputeBudgetLimits{}, FeeInfo: &fees.TxFeeInfo{ExecutionFee: 5_000, TotalFee: 5_000}, } - feeInfo, err := ApplyFeesOnlyTransaction(slotCtx, tx, output) + feeInfo, err := ApplyFailedTransaction(slotCtx, tx, output) assert.Nil(t, feeInfo) - assert.ErrorContains(t, err, "not a processable fees-only load failure") + assert.ErrorContains(t, err, "not an includable failure") payerAfter, err := slotCtx.GetAccount(txfixture.PayerPubkey()) require.NoError(t, err) assert.Equal(t, payerBefore.Lamports, payerAfter.Lamports) } -func TestApplyFeesOnlyTransactionRejectsIncompleteProcessingState(t *testing.T) { +func TestApplyFailedTransactionRejectsIncompleteProcessingState(t *testing.T) { slotCtx, cleanup := newCommitTestSlotCtx() defer cleanup() tx, err := solana.TransactionFromBytes(txfixture.MustSignedTransferWire(0)) @@ -177,7 +177,7 @@ func TestApplyFeesOnlyTransactionRejectsIncompleteProcessingState(t *testing.T) // FeeInfo intentionally omitted: this output did not complete the // validated fee-payer phase and is unsafe to publish. } - feeInfo, err := ApplyFeesOnlyTransaction(slotCtx, tx, output) + feeInfo, err := ApplyFailedTransaction(slotCtx, tx, output) assert.Nil(t, feeInfo) assert.ErrorContains(t, err, "missing required processing state") payerAfter, err := slotCtx.GetAccount(txfixture.PayerPubkey()) diff --git a/pkg/replay/includable_failure_test.go b/pkg/replay/includable_failure_test.go new file mode 100644 index 000000000..07949de77 --- /dev/null +++ b/pkg/replay/includable_failure_test.go @@ -0,0 +1,148 @@ +package replay + +import ( + "errors" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/fees" + "github.com/Overclock-Validator/mithril/pkg/rent" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" + "github.com/gagliardetto/solana-go" + computebudget "github.com/gagliardetto/solana-go/programs/compute-budget" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func failedTxOutput(errType TransactionErrorType, err error, executed bool) LoadAndExecuteTransactionOutput { + out := LoadAndExecuteTransactionOutput{ + ProcessingResult: TransactionProcessingResult{ + TransactionError: &TransactionError{ErrorType: errType, InstructionError: err}, + }, + ComputeBudgetLimits: &sealevel.ComputeBudgetLimits{}, + FeeInfo: &fees.TxFeeInfo{ExecutionFee: 5_000, TotalFee: 5_000}, + } + if executed { + out.ExecCtx = &sealevel.ExecutionCtx{} + } + return out +} + +func TestIncludableFailureClassification(t *testing.T) { + postExecRent := &rent.RentStateTransitionError{AccountIndex: 1, Err: errors.New("rent state")} + includable := []LoadAndExecuteTransactionOutput{ + failedTxOutput(TransactionErrorMaxLoadedAccountsDataSizeExceeded, nil, false), + failedTxOutput(TransactionErrorInvalidProgramForExecution, nil, false), + failedTxOutput(TransactionErrorProgramAccountNotFound, nil, false), + failedTxOutput(TransactionErrorInstructionError, sealevel.InstrErrInvalidArgument, true), + failedTxOutput(TransactionErrorInsufficientFundsForRent, postExecRent, true), + } + for _, out := range includable { + errType := out.ProcessingResult.TransactionError.ErrorType + assert.Truef(t, includableFailure(out), "%s", errType) + out.ProcessedAsNoOp = true + assert.Falsef(t, includableFailure(out), "%s no-op", errType) + } + + // The same types raised before execution are not includable. + assert.False(t, includableFailure(failedTxOutput(TransactionErrorInstructionError, sealevel.InstrErrInvalidArgument, false)), + "compute-budget instruction error") + assert.False(t, includableFailure(failedTxOutput(TransactionErrorInsufficientFundsForRent, fees.ErrInsufficientFundsForRent, false)), + "fee-payer rent error") + assert.False(t, includableFailure(failedTxOutput(TransactionErrorInsufficientFundsForRent, fees.ErrInsufficientFundsForRent, true)), + "fee-payer rent error on the loaded copy") + assert.False(t, includableFailure(LoadAndExecuteTransactionOutput{}), "success") + + for errType := TransactionErrorAccountInUse; errType <= TransactionErrorProgramCacheHitMaxLimit; errType++ { + switch errType { + case TransactionErrorMaxLoadedAccountsDataSizeExceeded, TransactionErrorInvalidProgramForExecution, + TransactionErrorProgramAccountNotFound, TransactionErrorInstructionError, TransactionErrorInsufficientFundsForRent: + continue + } + assert.Falsef(t, includableFailure(failedTxOutput(errType, postExecRent, true)), "%s", errType) + } +} + +func signedCommitTestTransaction(t *testing.T, instructions ...solana.Instruction) *solana.Transaction { + t.Helper() + tx, err := solana.NewTransaction(instructions, txfixture.TestBlockhash(), solana.TransactionPayer(txfixture.PayerPubkey())) + require.NoError(t, err) + payerKey := txfixture.PayerPrivateKey() + _, err = tx.Sign(func(solana.PublicKey) *solana.PrivateKey { return &payerKey }) + require.NoError(t, err) + return tx +} + +// TestIncludableFailureFromExecution classifies results the pipeline actually +// produces, so each error is checked at the stage that raises it. +func TestIncludableFailureFromExecution(t *testing.T) { + rentSysvar := sealevel.NewDefaultRentSysvar() + rentMin := rentSysvar.MinimumBalance(0) + newAccount := solana.PublicKey{0xEE} + cases := []struct { + name string + payer uint64 + tx func(t *testing.T) *solana.Transaction + want TransactionErrorType + includable bool + }{ + { + name: "instruction error during execution", + tx: func(t *testing.T) *solana.Transaction { return signedCommitTestTransaction(t, transfer(20_000_000_000, txfixture.DestPubkey())) }, + want: TransactionErrorInstructionError, + includable: true, + }, + { + name: "rent-state check after execution", + tx: func(t *testing.T) *solana.Transaction { return signedCommitTestTransaction(t, transfer(1, newAccount)) }, + want: TransactionErrorInsufficientFundsForRent, + includable: true, + }, + { + name: "fees-only load failure", + tx: func(t *testing.T) *solana.Transaction { + return signedCommitTestTransaction(t, + computebudget.NewSetLoadedAccountsDataSizeLimitInstruction(1).Build(), transfer(1, txfixture.DestPubkey())) + }, + want: TransactionErrorMaxLoadedAccountsDataSizeExceeded, + includable: true, + }, + { + name: "compute-budget instruction error before execution", + tx: func(t *testing.T) *solana.Transaction { + return signedCommitTestTransaction(t, + solana.NewInstruction(computebudget.ProgramID, nil, []byte{0xFF}), transfer(1, txfixture.DestPubkey())) + }, + want: TransactionErrorInstructionError, + }, + { + name: "fee-payer rent check before execution", + payer: rentMin + 5_000 - 1, + tx: func(t *testing.T) *solana.Transaction { return signedCommitTestTransaction(t, transfer(1, txfixture.DestPubkey())) }, + want: TransactionErrorInsufficientFundsForRent, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + slotCtx, cleanup := newCommitTestSlotCtx() + defer cleanup() + if tc.payer != 0 { + payer, err := slotCtx.GetAccount(txfixture.PayerPubkey()) + require.NoError(t, err) + payer.Lamports = tc.payer + require.NoError(t, slotCtx.SetAccount(txfixture.PayerPubkey(), payer)) + } + output := LoadAndExecuteTransaction(LoadAndExecuteTransactionInput{ + SlotCtx: slotCtx, Transaction: tc.tx(t), LeanResult: true, + }) + require.NotNil(t, output.ProcessingResult.TransactionError) + require.Equal(t, tc.want, output.ProcessingResult.TransactionError.ErrorType) + assert.Equal(t, tc.includable, includableFailure(output)) + }) + } +} + +func transfer(lamports uint64, to solana.PublicKey) solana.Instruction { + return system.NewTransferInstruction(lamports, txfixture.PayerPubkey(), to).Build() +}