Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,33 @@ type BlockReplay struct {
}

var GlobalBlockReplay = BlockReplay{}

// Counter is a monotonic atomic counter for events. Operations are
// lock-free; use it for high-frequency increments where Timing's
// nanosecond accumulator is unnecessary.
type Counter struct {
value uint64
}

func (c *Counter) Inc() {
atomic.AddUint64(&c.value, 1)
}

func (c *Counter) Get() uint64 {
return atomic.LoadUint64(&c.value)
}

// Simulate tracks RPC simulateTransaction handler events. Counters are
// surfaced through the standard metrics endpoint; latency uses the same
// Timing type as block replay so dashboards can reuse existing rendering.
type Simulate struct {
TotalCalls Counter
SanitizeFailures Counter
AddressLookupFails Counter
NonceFallbackHits Counter
Errors Counter
Successes Counter
HandlerLatency Timing
}

var GlobalSimulate = Simulate{}
75 changes: 75 additions & 0 deletions pkg/replay/inner_instructions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package replay

import (
"testing"

"github.com/Overclock-Validator/mithril/pkg/sealevel"
"github.com/stretchr/testify/assert"
)

func TestAssembleInnerInstructions_EmptyReturnsNil(t *testing.T) {
execCtx := &sealevel.ExecutionCtx{}
got := AssembleInnerInstructions(execCtx)
assert.Nil(t, got)
}

func TestAssembleInnerInstructions_GroupsByTopLevelIndex(t *testing.T) {
execCtx := &sealevel.ExecutionCtx{
InnerInstrs: []sealevel.RecordedInnerInstr{
{TopLevelIdx: 0, StackHeight: 2, ProgramIdIndex: 5, Accounts: []uint8{1, 2}, Data: []byte{0xAA}},
{TopLevelIdx: 0, StackHeight: 3, ProgramIdIndex: 6, Accounts: []uint8{3}, Data: []byte{0xBB}},
{TopLevelIdx: 2, StackHeight: 2, ProgramIdIndex: 7, Accounts: []uint8{4, 5}, Data: []byte{0xCC}},
},
}

got := AssembleInnerInstructions(execCtx)
assert.Len(t, got, 2)

assert.Equal(t, uint8(0), got[0].Index)
assert.Len(t, got[0].Instructions, 2)
assert.Equal(t, uint8(5), got[0].Instructions[0].ProgramIdIndex)
assert.Equal(t, []uint8{1, 2}, got[0].Instructions[0].Accounts)
assert.Equal(t, []byte{0xAA}, got[0].Instructions[0].Data)
assert.Equal(t, uint8(2), got[0].Instructions[0].StackHeight)
assert.Equal(t, uint8(6), got[0].Instructions[1].ProgramIdIndex)
assert.Equal(t, uint8(3), got[0].Instructions[1].StackHeight)

assert.Equal(t, uint8(2), got[1].Index)
assert.Len(t, got[1].Instructions, 1)
assert.Equal(t, uint8(7), got[1].Instructions[0].ProgramIdIndex)
assert.Equal(t, uint8(2), got[1].Instructions[0].StackHeight)
}

func TestAssembleInnerInstructions_PreservesInsertionOrder(t *testing.T) {
execCtx := &sealevel.ExecutionCtx{
InnerInstrs: []sealevel.RecordedInnerInstr{
{TopLevelIdx: 3},
{TopLevelIdx: 1},
{TopLevelIdx: 3},
{TopLevelIdx: 0},
},
}
got := AssembleInnerInstructions(execCtx)
indices := []uint8{got[0].Index, got[1].Index, got[2].Index}
assert.Equal(t, []uint8{3, 1, 0}, indices,
"order must reflect first-seen top-level index, not numeric sort")
}

func TestAssembleInnerInstructions_DefensiveCopySlices(t *testing.T) {
original := sealevel.RecordedInnerInstr{
TopLevelIdx: 0,
ProgramIdIndex: 1,
Accounts: []uint8{1, 2},
Data: []byte{0x01, 0x02},
}
execCtx := &sealevel.ExecutionCtx{
InnerInstrs: []sealevel.RecordedInnerInstr{original},
}
got := AssembleInnerInstructions(execCtx)

// Mutating the assembled output must not corrupt the source.
got[0].Instructions[0].Accounts[0] = 99
got[0].Instructions[0].Data[0] = 0xFF
assert.Equal(t, uint8(1), execCtx.InnerInstrs[0].Accounts[0])
assert.Equal(t, byte(0x01), execCtx.InnerInstrs[0].Data[0])
}
179 changes: 179 additions & 0 deletions pkg/replay/simulate_fixtures_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package replay

import (
"encoding/base64"
"testing"

"github.com/Overclock-Validator/mithril/pkg/accounts"
"github.com/Overclock-Validator/mithril/pkg/features"
"github.com/Overclock-Validator/mithril/pkg/sealevel"
"github.com/gagliardetto/solana-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Fixtures captured from live mainnet RPC validation. Each base64 string is
// a serialized transaction that exercises a specific simulate-path branch.
// Constants live in the test file so the suite is self-contained and runs
// without any external state.
const (
// Single transfer with a missing recipient/system pubkey. Without the
// SIMD-186 fabricate-default fix, the loader panicked.
fixtureMissingAccount = "ARH9cDfHfnReizlJ7wUiQUDjBHEmFqxOwkrd58jKTtaSiWDD12ojfj+Q6EFIZdtZPEhjaFBjGEKaso9l8vStIgoBAAEDm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzLlsqZyE2J9rbpagOWcYxTmla0sPZSdLWedHDC5wCQ+QwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAAQAAAAAAAAA="

// Tx with NumRequiredSignatures=0 — sanitize must reject before
// anything tries to index Signatures[0] / AccountKeys[0].
fixtureZeroSig = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

// Out-of-range instruction account index.
fixtureOOBAccount = "AcvGOK/HCvm+jp0iIyg/CBjxoYXidL7Eua6PZr/gYq6SnXSwFBBClGqoT3Dq8SkekT05ydTutpEnhaCXZTkyoQQBAAABm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAASoA"

// NumReadonlySignedAccounts >= NumRequiredSignatures (no writable
// signer); sanitize must reject.
fixtureZeroWritableSigners = "AXP1E+UKbcHEW0T4v81ty2POfQ3VR7omBlei30O7AjHX/Me4P2Zidwhl8acF3jrYONVVV2kv8bbCjyLhq/SZVAYBAQABm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

// Header declares 2 required sigs, transaction carries 1.
fixtureInsufficientSigs = "AVW49xPgdaW13hcaexkKxn9aruubS1QPM2i4Gfyvcj+/ZCml3HapzjPxsKXvpsph6bhHIziIQS+wW7il9prJ4AgCAAACm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzLlsqZyE2J9rbpagOWcYxTmla0sPZSdLWedHDC5wCQ+QwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="

// 2 signatures but only 1 account key.
fixtureSigsGreaterThanKeys = "AkoD9kRSzxiTCsWNc0tek1JI5Capj2LNsTI0MTUTYwuF15j7urkfyWvnyPQ87UHzaMHkChQ3TA7CBhuNwmojVgVKA/ZEUs8YkwrFjXNLXpNSSOQmqY9izbEyNDE1E2MLhdeY+7q5H8lr58j0PO1B82jB5AoUN0wOwgYbjcJqI1YFAQAAAZuXUt5eJ3b6DDxsAIGw2tnrVeG2vBoRZsxBKFi50FMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

// ProgramIDIndex points outside AccountKeys range.
fixtureOOBProgram = "AT2SwP5wteXeZK5dcicARe9tPzofzhoUDRBgMS+By+o8CwEidmOMyFLPlqDk1AY8PP0dk5u9zkBCCAhiRXRfVQIBAAABm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFjAAA="

// Tx with no instructions. Should pass sanitize and proceed to
// fee-payer balance check; payer has 0 SOL → InsufficientFundsForFee.
fixtureNoInstructions = "AUoD9kRSzxiTCsWNc0tek1JI5Capj2LNsTI0MTUTYwuF15j7urkfyWvnyPQ87UHzaMHkChQ3TA7CBhuNwmojVgUBAAABm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

// Self-transfer: fee payer also receives.
fixtureSelfTransfer = "AQaoIo/np/BZLN6khLJM2yEybInNs68QaywxRtscCSguw5/qEYsSrYorN0y0zWSGaGrgFrlD5Oaz96CMsAO3iw8BAAECm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAAMAgAAAAEAAAAAAAAA"

// Duplicate AccountKeys entries.
fixtureDuplicateKeys = "ARL1Vif1R7PRgEj1eZZzM1uhhoiPseT/kr/MjAVxurE8GtpLADDL6pfDVEege4anhvnZC/dMZPlRkTTgSUr2awIBAAEDm5dS3l4ndvoMPGwAgbDa2etV4ba8GhFmzEEoWLnQUzKbl1LeXid2+gw8bACBsNrZ61XhtrwaEWbMQShYudBTMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAAQAAAAAAAAA="
)

func decodeFixtureTx(t *testing.T, b64 string) *solana.Transaction {
t.Helper()
raw, err := base64.StdEncoding.DecodeString(b64)
require.NoError(t, err)
tx, err := solana.TransactionFromBytes(raw)
require.NoError(t, err)
return tx
}

// simulateFixtureSlotCtx returns a SlotCtx with the SIMD-186 feature
// enabled and an empty MemAccounts. Suitable for fixtures that exercise
// the sanitize / loader path without needing a populated accountsdb.
//
// FeeRateGovernor is non-nil because LoadAndExecuteTransaction reaches
// for it during execution context construction; production callers
// always set it.
func simulateFixtureSlotCtx() *sealevel.SlotCtx {
feats := features.NewFeaturesDefault()
feats.EnableFeature(features.FormalizeLoadedTransactionDataSize, 0)
return &sealevel.SlotCtx{
Features: feats,
Accounts: accounts.NewMemAccounts(),
FeeRateGovernor: &sealevel.FeeRateGovernor{},
}
}

func runSimulateFixture(t *testing.T, b64 string) LoadAndExecuteTransactionOutput {
t.Helper()
defer withEmptyRecentBlockhashesSysvarFixture()()
return LoadAndExecuteTransaction(LoadAndExecuteTransactionInput{
SlotCtx: simulateFixtureSlotCtx(),
Transaction: decodeFixtureTx(t, b64),
IsSimulation: true,
})
}

// withEmptyRecentBlockhashesSysvarFixture seeds the global cache so
// IsTransactionAgeValid does not nil-deref. Production seeds this via
// cacheConstantSysvars before any RPC call.
func withEmptyRecentBlockhashesSysvarFixture() func() {
prev := sealevel.SysvarCache.RecentBlockHashes.Sysvar
empty := sealevel.SysvarRecentBlockhashes{}
sealevel.SysvarCache.RecentBlockHashes.Sysvar = &empty
return func() { sealevel.SysvarCache.RecentBlockHashes.Sysvar = prev }
}

// Without a populated AccountsDb, the System Program is unreachable and
// the loader rejects with ProgramAccountNotFound. With AccountsDb (the
// production case), the Pass-1 fallback resolves it and the tx fails
// later at the fee-payer balance check (InsufficientFundsForFee). Either
// outcome is a clean TransactionError, never a panic.
func TestSimulateFixture_MissingAccount(t *testing.T) {
out := runSimulateFixture(t, fixtureMissingAccount)
require.NotNil(t, out.ProcessingResult.TransactionError)
got := out.ProcessingResult.TransactionError.ErrorType
assert.Contains(t,
[]TransactionErrorType{TransactionErrorProgramAccountNotFound, TransactionErrorInsufficientFundsForFee},
got,
"expected ProgramAccountNotFound (no AccountsDb) or InsufficientFundsForFee (with AccountsDb), got %v", got,
)
}

func TestSimulateFixture_ZeroSig(t *testing.T) {
out := runSimulateFixture(t, fixtureZeroSig)
require.NotNil(t, out.ProcessingResult.TransactionError)
assert.Equal(t, TransactionErrorSanitizeFailure, out.ProcessingResult.TransactionError.ErrorType)
}

func TestSimulateFixture_OOBAccount(t *testing.T) {
out := runSimulateFixture(t, fixtureOOBAccount)
require.NotNil(t, out.ProcessingResult.TransactionError)
assert.Equal(t, TransactionErrorSanitizeFailure, out.ProcessingResult.TransactionError.ErrorType)
}

func TestSimulateFixture_ZeroWritableSigners(t *testing.T) {
out := runSimulateFixture(t, fixtureZeroWritableSigners)
require.NotNil(t, out.ProcessingResult.TransactionError)
assert.Equal(t, TransactionErrorSanitizeFailure, out.ProcessingResult.TransactionError.ErrorType)
}

func TestSimulateFixture_InsufficientSigs(t *testing.T) {
out := runSimulateFixture(t, fixtureInsufficientSigs)
require.NotNil(t, out.ProcessingResult.TransactionError)
assert.Equal(t, TransactionErrorSanitizeFailure, out.ProcessingResult.TransactionError.ErrorType)
}

func TestSimulateFixture_SigsGreaterThanKeys(t *testing.T) {
out := runSimulateFixture(t, fixtureSigsGreaterThanKeys)
require.NotNil(t, out.ProcessingResult.TransactionError)
assert.Equal(t, TransactionErrorSanitizeFailure, out.ProcessingResult.TransactionError.ErrorType)
}

func TestSimulateFixture_OOBProgram(t *testing.T) {
out := runSimulateFixture(t, fixtureOOBProgram)
require.NotNil(t, out.ProcessingResult.TransactionError)
assert.Equal(t, TransactionErrorSanitizeFailure, out.ProcessingResult.TransactionError.ErrorType)
}

func TestSimulateFixture_NoInstructions(t *testing.T) {
out := runSimulateFixture(t, fixtureNoInstructions)
require.NotNil(t, out.ProcessingResult.TransactionError)
assert.Equal(t, TransactionErrorInsufficientFundsForFee, out.ProcessingResult.TransactionError.ErrorType)
}

// Same caveat as MissingAccount: with no AccountsDb the System Program
// is unreachable, so the loader fails earlier than the fee-payer check.
func TestSimulateFixture_SelfTransfer(t *testing.T) {
out := runSimulateFixture(t, fixtureSelfTransfer)
require.NotNil(t, out.ProcessingResult.TransactionError)
got := out.ProcessingResult.TransactionError.ErrorType
assert.Contains(t,
[]TransactionErrorType{TransactionErrorProgramAccountNotFound, TransactionErrorInsufficientFundsForFee},
got,
)
}

func TestSimulateFixture_DuplicateKeys(t *testing.T) {
out := runSimulateFixture(t, fixtureDuplicateKeys)
require.NotNil(t, out.ProcessingResult.TransactionError)
got := out.ProcessingResult.TransactionError.ErrorType
assert.Contains(t,
[]TransactionErrorType{TransactionErrorProgramAccountNotFound, TransactionErrorInsufficientFundsForFee},
got,
)
}
Loading
Loading