From b006f950fde80d3f988642eb23d558ab73bb9b3c Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:16:07 -0500 Subject: [PATCH 1/9] Improve leader packing and add near-limit synthetic block tests --- cmd/mithril/node/node.go | 91 +++++---- config.example.toml | 9 + pkg/accounts/mem_accounts.go | 15 +- pkg/accounts/mem_accounts_test.go | 15 ++ pkg/blockprod/bank.go | 52 ++++- pkg/blockprod/leader.go | 37 +++- .../leader_completion_reserve_test.go | 32 +++ pkg/blockprod/leader_processing_test.go | 81 ++++++++ pkg/blockprod/leader_throughput_bench_test.go | 117 +++++++++++ pkg/blockprod/prepared_transaction_test.go | 142 +++++++++++++ pkg/blockprod/readonly_block_bench_test.go | 187 ++++++++++++++++++ .../readonly_block_prepared_bench_test.go | 45 +++++ pkg/blockprod/readonly_load_bench_test.go | 95 +++++++++ pkg/blockprod/scheduler/buffer.go | 63 ++++-- pkg/blockprod/scheduler/buffer_bench_test.go | 45 +++++ pkg/blockprod/scheduler/buffer_order_test.go | 105 ++++++++++ pkg/blockprod/scheduler/config_test.go | 28 +++ pkg/blockprod/scheduler/scheduler.go | 56 ++++-- pkg/blockprod/scheduler/scheduler_test.go | 20 +- pkg/config/config.go | 2 + pkg/costmodel/entry_bytes.go | 7 +- pkg/costmodel/limits.go | 48 ++++- pkg/costmodel/slot_limits_test.go | 88 +++++++++ pkg/costmodel/transaction_cost.go | 15 +- pkg/merkletree/merkletree.go | 44 ++++- pkg/merkletree/root_test.go | 60 ++++++ pkg/metrics/metrics.go | 12 +- pkg/replay/chain_state.go | 11 ++ pkg/replay/transaction_preparation.go | 133 +++++++++++++ pkg/replay/transaction_processing_pure.go | 134 +++++++------ pkg/sealevel/execution_ctx.go | 16 +- pkg/tpu/txfixture/readonly_pair.go | 42 ++++ pkg/tpu/txfixture/readonly_pair_test.go | 68 +++++++ pkg/turbine/entry_hash.go | 9 +- pkg/turbine/entry_hash_bench_test.go | 24 +++ 35 files changed, 1770 insertions(+), 178 deletions(-) create mode 100644 pkg/blockprod/leader_completion_reserve_test.go create mode 100644 pkg/blockprod/leader_processing_test.go create mode 100644 pkg/blockprod/leader_throughput_bench_test.go create mode 100644 pkg/blockprod/prepared_transaction_test.go create mode 100644 pkg/blockprod/readonly_block_bench_test.go create mode 100644 pkg/blockprod/readonly_block_prepared_bench_test.go create mode 100644 pkg/blockprod/readonly_load_bench_test.go create mode 100644 pkg/blockprod/scheduler/buffer_bench_test.go create mode 100644 pkg/blockprod/scheduler/buffer_order_test.go create mode 100644 pkg/blockprod/scheduler/config_test.go create mode 100644 pkg/costmodel/slot_limits_test.go create mode 100644 pkg/merkletree/root_test.go create mode 100644 pkg/replay/transaction_preparation.go create mode 100644 pkg/tpu/txfixture/readonly_pair.go create mode 100644 pkg/tpu/txfixture/readonly_pair_test.go create mode 100644 pkg/turbine/entry_hash_bench_test.go diff --git a/cmd/mithril/node/node.go b/cmd/mithril/node/node.go index e23d9682b..a7c95f9e2 100644 --- a/cmd/mithril/node/node.go +++ b/cmd/mithril/node/node.go @@ -74,35 +74,37 @@ var ( }, } - bootstrapMode string // "auto", "snapshot", "new-snapshot", "new-incremental", or "accountsdb" - snapshotArchivePath string - incrementalSnapshotFilename string - accountsPath string - scratchDirectory string - rpcEndpoints []string - cluster string // "alpenglow", "mainnet-beta", "testnet", or "devnet" - legacyGenesisHash string // explicit lineage for pre-binding AccountsDB/ledger artifacts - blockSource string // "turbine", "rpc", or "lightbringer" - lightbringerEndpoint string - repairCatchupMaxGapSlots int // Resume gaps up to this fill via turbine repair instead of RPC (0 = off) - repairMaxRequestsPerSecond int // Repair request-rate ceiling override (0 = adaptive default) - blockRPCFallback bool // Allow RPC block fetch when > repairCatchupMaxGapSlots behind (default false: shreds only) - blockMaxRPS int // Rate limit for block fetching - blockMaxInflight int // Max concurrent block fetch workers - blockTipPollIntervalMs int // Tip poll interval in milliseconds - blockTipSafetyMargin int // Don't fetch within N slots of tip - consensusModeFlag string // raw --consensus-mode value (cobra binding) - consensusMode string // resolved: "verifying" (default) or "validator" - alpenglowObserverBindAddr string - alpenglowMaxMessageBytes int64 - alpenglowBLSDST string - validatorIdentityKeypair string - validatorVoteAccountKeypair string - validatorAuthorizedVoterKeypair string - validatorWithdrawerKeypair string - validatorTPUQUICBind string - validatorAdvertisedIP string - validatorSigverifyWorkers int + bootstrapMode string // "auto", "snapshot", "new-snapshot", "new-incremental", or "accountsdb" + snapshotArchivePath string + incrementalSnapshotFilename string + accountsPath string + scratchDirectory string + rpcEndpoints []string + cluster string // "alpenglow", "mainnet-beta", "testnet", or "devnet" + legacyGenesisHash string // explicit lineage for pre-binding AccountsDB/ledger artifacts + blockSource string // "turbine", "rpc", or "lightbringer" + lightbringerEndpoint string + repairCatchupMaxGapSlots int // Resume gaps up to this fill via turbine repair instead of RPC (0 = off) + repairMaxRequestsPerSecond int // Repair request-rate ceiling override (0 = adaptive default) + blockRPCFallback bool // Allow RPC block fetch when > repairCatchupMaxGapSlots behind (default false: shreds only) + blockMaxRPS int // Rate limit for block fetching + blockMaxInflight int // Max concurrent block fetch workers + blockTipPollIntervalMs int // Tip poll interval in milliseconds + blockTipSafetyMargin int // Don't fetch within N slots of tip + consensusModeFlag string // raw --consensus-mode value (cobra binding) + consensusMode string // resolved: "verifying" (default) or "validator" + alpenglowObserverBindAddr string + alpenglowMaxMessageBytes int64 + alpenglowBLSDST string + validatorIdentityKeypair string + validatorVoteAccountKeypair string + validatorAuthorizedVoterKeypair string + validatorWithdrawerKeypair string + validatorTPUQUICBind string + validatorAdvertisedIP string + validatorSigverifyWorkers int + validatorCompletionReserveMs int + validatorMaxBufferedTransactions int // Mode thresholds blockNearTipThreshold int // Enter near-tip when gap <= this @@ -547,6 +549,8 @@ func init() { Run.Flags().StringVar(&validatorTPUQUICBind, "tpu-quic-bind-addr", "", "Validator TPU QUIC listen address (default 0.0.0.0:8004)") Run.Flags().StringVar(&validatorAdvertisedIP, "validator-advertised-ip", "", "Public IP advertised for validator TPU QUIC") Run.Flags().IntVar(&validatorSigverifyWorkers, "tpu-sigverify-workers", 0, "TPU signature verification workers (0 = GOMAXPROCS)") + Run.Flags().IntVar(&validatorCompletionReserveMs, "leader-completion-reserve-ms", 0, "Time reserved for leader finalization and broadcast (0 = 75ms default; tune from measured completion times)") + Run.Flags().IntVar(&validatorMaxBufferedTransactions, "tpu-max-buffered-transactions", 0, "Maximum queued TPU transactions (0 = 131072 default)") // [tuning] section flags Run.Flags().Uint64Var(¶mArenaSizeMB, "param-arena-size-mb", 512, "Size in MB for serialized parameter arena (0 to disable)") @@ -869,6 +873,14 @@ func initConfigAndBindFlags(cmd *cobra.Command) error { } validatorAdvertisedIP = getString("validator-advertised-ip", "validator.advertised_ip") validatorSigverifyWorkers = getInt("tpu-sigverify-workers", "validator.tpu_sigverify_workers") + validatorCompletionReserveMs = getInt("leader-completion-reserve-ms", "validator.block_completion_reserve_ms") + validatorMaxBufferedTransactions = getInt("tpu-max-buffered-transactions", "validator.tpu_max_buffered_transactions") + if validatorMaxBufferedTransactions < 0 { + return fmt.Errorf("TPU maximum buffered transactions must be nonnegative") + } + if validatorCompletionReserveMs < 0 || validatorCompletionReserveMs >= int(blockprod.AlpenglowSlotDuration/time.Millisecond) { + return fmt.Errorf("leader completion reserve must be 0 (default) or between 1 and 199 milliseconds") + } // [block] section blockSource = getString("block-source", "block.source") @@ -2718,7 +2730,9 @@ postBootstrap: defer broadcaster.Close() controller := blockprod.NewController() - topicSink := scheduler.New(controller) + topicSink := scheduler.NewWithConfig(controller, scheduler.Config{ + FeatureSource: replay.ChainTipFeatures, MaxBufferedTransactions: validatorMaxBufferedTransactions, + }) topicSink.Start(ctx) defer topicSink.Stop() tpuCfg := tpu.DefaultConfig() @@ -2765,14 +2779,15 @@ postBootstrap: leaderStop := make(chan struct{}) leaderDone := make(chan struct{}) leaderLoop := blockprod.NewLeaderLoop(blockprod.LeaderLoopConfig{ - Controller: controller, - Identity: solana.PrivateKey(validatorIdentity), - AccountsDb: accountsDb, - Broadcaster: broadcaster, - ShredVersion: uint16(turbineShredVersion), - EpochSchedule: epochSchedule, - AlpenglowClock: true, - SlotDuration: blockprod.AlpenglowSlotDuration, + Controller: controller, + Identity: solana.PrivateKey(validatorIdentity), + AccountsDb: accountsDb, + Broadcaster: broadcaster, + ShredVersion: uint16(turbineShredVersion), + EpochSchedule: epochSchedule, + AlpenglowClock: true, + SlotDuration: blockprod.AlpenglowSlotDuration, + CompletionReserve: time.Duration(validatorCompletionReserveMs) * time.Millisecond, ParentContext: func(slot uint64) blockprod.ParentContext { tip := replay.ChainTipParentContext() // Blockprod owns the replay-readiness rule. In particular, the first diff --git a/config.example.toml b/config.example.toml index 2636e1b13..2cf1affb9 100644 --- a/config.example.toml +++ b/config.example.toml @@ -328,6 +328,15 @@ name = "mithril" # Signature-verification workers (0 = GOMAXPROCS). tpu_sigverify_workers = 0 + # Bounded cross-slot TPU queue. Zero keeps the 131,072-transaction default. + # Larger queues can prefill four busy leader slots, using additional memory. + tpu_max_buffered_transactions = 0 + + # Milliseconds reserved for local finalization and broadcast, not consensus + # finality. Zero keeps the conservative 75ms default. Tune from measured + # completion margins; this does not change the protocol slot deadline. + block_completion_reserve_ms = 0 + # ============================================================================ # [consensus] - Alpenglow Consensus # ============================================================================ diff --git a/pkg/accounts/mem_accounts.go b/pkg/accounts/mem_accounts.go index 4158be553..630ffb420 100644 --- a/pkg/accounts/mem_accounts.go +++ b/pkg/accounts/mem_accounts.go @@ -1,7 +1,6 @@ package accounts import ( - "fmt" "sync" "github.com/Overclock-Validator/mithril/pkg/base58" @@ -13,6 +12,16 @@ type MemAccounts struct { mu *sync.RWMutex } +// A miss is an ordinary step when falling back to parent accounts. Defer the +// diagnostic encoding until it is needed, and copy the key so callers can reuse it. +type missingMemAccountError struct { + key [32]byte +} + +func (e *missingMemAccountError) Error() string { + return "no such account " + base58.Encode(e.key[:]) + " found" +} + func NewMemAccounts() MemAccounts { return MemAccounts{ Map: make(map[[32]byte]*Account), @@ -32,7 +41,7 @@ func (m MemAccounts) GetAccount(pubkey *[32]byte) (*Account, error) { defer m.mu.RUnlock() acct, ok := m.Map[*pubkey] if !ok { - return nil, fmt.Errorf("no such account %s found", base58.Encode(pubkey[:])) + return nil, &missingMemAccountError{key: *pubkey} } return acct, nil } @@ -40,7 +49,7 @@ func (m MemAccounts) GetAccount(pubkey *[32]byte) (*Account, error) { func (m MemAccounts) GetAccountWithoutLock(pubkey solana.PublicKey) (*Account, error) { acct, ok := m.Map[pubkey] if !ok { - return nil, fmt.Errorf("no such account %s found", base58.Encode(pubkey[:])) + return nil, &missingMemAccountError{key: pubkey} } return acct, nil } diff --git a/pkg/accounts/mem_accounts_test.go b/pkg/accounts/mem_accounts_test.go index bd55cb218..4a614dd9f 100644 --- a/pkg/accounts/mem_accounts_test.go +++ b/pkg/accounts/mem_accounts_test.go @@ -3,8 +3,23 @@ package accounts import ( "testing" "time" + + "github.com/gagliardetto/solana-go" ) +func TestMemAccountMissingErrorRetainsLookupKey(t *testing.T) { + mem := NewMemAccounts() + key := [32]byte{} + _, lockedErr := mem.GetAccount(&key) + _, unlockedErr := mem.GetAccountWithoutLock(solana.PublicKey(key)) + key[0] = 99 // Lookup callers may reuse their key storage before reporting an error. + for _, err := range []error{lockedErr, unlockedErr} { + if err == nil || err.Error() != "no such account 11111111111111111111111111111111 found" { + t.Fatalf("missing error lost its original key: %v", err) + } + } +} + func TestMemAccountsReadsAreConcurrent(t *testing.T) { mem := NewMemAccounts() var key [32]byte diff --git a/pkg/blockprod/bank.go b/pkg/blockprod/bank.go index 26784fdf5..b93e07d48 100644 --- a/pkg/blockprod/bank.go +++ b/pkg/blockprod/bank.go @@ -3,6 +3,7 @@ package blockprod import ( "sync" + "github.com/Overclock-Validator/mithril/pkg/arena" "github.com/Overclock-Validator/mithril/pkg/costmodel" "github.com/Overclock-Validator/mithril/pkg/features" "github.com/Overclock-Validator/mithril/pkg/fees" @@ -44,6 +45,10 @@ type WorkingBank struct { // seenMessages is the bank-local AlreadyProcessed status set. The TPU's // signature LRU is only an ingress optimization and is not authoritative. seenMessages map[[32]byte]struct{} + // Execution and commit are serialized by mu. Borrowed accounts never escape + // either phase, so their storage can be reset for the next transaction. + borrowedAccounts *arena.Arena[sealevel.BorrowedAccount] + preparer *replay.TransactionPreparer } type BankConfig struct { @@ -71,7 +76,12 @@ func NewWorkingBank(cfg BankConfig) *WorkingBank { if sink == nil { sink = NopBatchSink{} } + var preparer *replay.TransactionPreparer + if cfg.SlotCtx != nil { + preparer = replay.NewTransactionPreparer(cfg.SlotCtx.Features) + } return &WorkingBank{ + preparer: preparer, slotCtx: cfg.SlotCtx, slot: cfg.Slot, leader: cfg.Leader, @@ -82,6 +92,7 @@ func NewWorkingBank(cfg BankConfig) *WorkingBank { accepting: true, ancestorStatuses: cfg.TransactionStatuses, seenMessages: make(map[[32]byte]struct{}), + borrowedAccounts: arena.New[sealevel.BorrowedAccount](64), } } @@ -175,11 +186,30 @@ func (b *WorkingBank) Forge(wire []byte) (ForgeResult, costmodel.ExceedReason) { // ForgeTransaction executes and commits a parsed transaction. func (b *WorkingBank) ForgeTransaction(tx *solana.Transaction, wireSize int) (ForgeResult, costmodel.ExceedReason) { + return b.forgeTransaction(tx, wireSize, nil) +} + +// ForgePreparedTransaction reuses static work from owned, immutable TPU bytes. +// A different bank feature snapshot falls back to the ordinary execution path. +func (b *WorkingBank) ForgePreparedTransaction(tx *solana.Transaction, wireSize int, prepared *replay.PreparedTransaction) (ForgeResult, costmodel.ExceedReason) { + if b.slotCtx == nil || !b.preparer.Matches(prepared, tx, b.slotCtx.Features) { + prepared = nil + } + return b.forgeTransaction(tx, wireSize, prepared) +} + +func (b *WorkingBank) forgeTransaction(tx *solana.Transaction, wireSize int, prepared *replay.PreparedTransaction) (ForgeResult, costmodel.ExceedReason) { if tx == nil { b.RebateSchedule(wireSize) return ForgeDroppedParse, costmodel.ExceedNone } - messageHash, err := replay.TransactionMessageHash(tx) + var messageHash [32]byte + var err error + if prepared != nil { + messageHash = prepared.MessageHash() + } else { + messageHash, err = replay.TransactionMessageHash(tx) + } if err != nil { b.RebateSchedule(wireSize) return ForgeDroppedParse, costmodel.ExceedNone @@ -201,7 +231,11 @@ func (b *WorkingBank) ForgeTransaction(tx *solana.Transaction, wireSize int) (Fo f := features.NewFeaturesDefault() feats = f } - cost, err = costmodel.EstimateTransactionCost(tx, feats) + if prepared != nil { + cost = prepared.Cost() + } else { + cost, err = costmodel.EstimateTransactionCost(tx, feats) + } if err != nil { b.RebateSchedule(wireSize) return ForgeDroppedParse, costmodel.ExceedNone @@ -238,15 +272,17 @@ func (b *WorkingBank) ForgeTransaction(tx *solana.Transaction, wireSize int) (Fo if reason := b.reserveEntryBytesLocked(wireSize); reason != costmodel.ExceedNone { return ForgeDroppedCost, reason } - if err := fees.PayerCanFund(b.slotCtx, tx); err != nil { + if err := b.preparer.PayerCanFund(b.slotCtx, tx, prepared); err != nil { return ForgeDroppedExecution, costmodel.ExceedNone } - output := replay.LoadAndExecuteTransaction(replay.LoadAndExecuteTransactionInput{ - SlotCtx: b.slotCtx, - Transaction: tx, - LeanResult: true, - }) + output := b.preparer.LoadAndExecute(replay.LoadAndExecuteTransactionInput{ + SlotCtx: b.slotCtx, + Transaction: tx, + LeanResult: true, + SkipTimingMetrics: true, + Arena: b.borrowedAccounts, + }, prepared) if output.ProcessingResult.TransactionError != nil { feeInfo, err := replay.ApplyFeesOnlyTransaction(b.slotCtx, tx, output) if err != nil { diff --git a/pkg/blockprod/leader.go b/pkg/blockprod/leader.go index e304ffb8d..66bce23ee 100644 --- a/pkg/blockprod/leader.go +++ b/pkg/blockprod/leader.go @@ -103,11 +103,12 @@ type LeaderLoop struct { currentSlot func() uint64 leaderForSlot func(uint64) (solana.PublicKey, bool) - pollInterval time.Duration - slotDuration time.Duration - now func() time.Time - tickStartedAt time.Time - tickDeliveryLag time.Duration + pollInterval time.Duration + slotDuration time.Duration + completionReserve time.Duration + now func() time.Time + tickStartedAt time.Time + tickDeliveryLag time.Duration mu sync.Mutex activeSlot uint64 @@ -154,7 +155,10 @@ type LeaderLoopConfig struct { RewardCerts RewardCertBuilder PollInterval time.Duration SlotDuration time.Duration - Now func() time.Time + // CompletionReserve is time retained for finalization and broadcast. Zero + // uses the conservative default; tune only from measured completion times. + CompletionReserve time.Duration + Now func() time.Time } func NewLeaderLoop(cfg LeaderLoopConfig) *LeaderLoop { @@ -170,6 +174,9 @@ func NewLeaderLoop(cfg LeaderLoopConfig) *LeaderLoop { if cfg.Now == nil { cfg.Now = time.Now } + if cfg.CompletionReserve <= 0 { + cfg.CompletionReserve = leaderBlockCompletionReserve + } return &LeaderLoop{ controller: cfg.Controller, identity: cfg.Identity, @@ -187,6 +194,7 @@ func NewLeaderLoop(cfg LeaderLoopConfig) *LeaderLoop { rewardCerts: cfg.RewardCerts, pollInterval: cfg.PollInterval, slotDuration: cfg.SlotDuration, + completionReserve: cfg.CompletionReserve, now: cfg.Now, finishedLeaderSlots: make(map[uint64]struct{}), pendingFailures: make(map[uint64]leaderSlotFailure), @@ -329,9 +337,11 @@ func (l *LeaderLoop) tickScheduled(scheduledAt time.Time) { delete(l.pendingFailures, targetSlot) openedAt := l.now() l.recordTickTimingLocked(openedAt, "opened") - mlog.Log.InfofPrecise("ALPENGLOW block production: opened local leader slot=%d parent_slot=%d replay_frontier=%d live_slot=%d start_slot_ms=%d%s", + limits := l.activeBank.CostTracker().Limits() + mlog.Log.InfofPrecise("ALPENGLOW block production: opened local leader slot=%d parent_slot=%d replay_frontier=%d live_slot=%d start_slot_ms=%d block_cost_limit=%d account_cost_limit=%d entry_bytes_limit=%d%s", targetSlot, l.parentCtx.ParentSlot, global.ReplayFrontier(), wallSlot, - startDuration.Milliseconds(), l.productionStartTimingDetailLocked(targetSlot, openedAt)) + startDuration.Milliseconds(), limits.BlockCost, limits.WritableAccountCost, limits.MaxEntryBytes, + l.productionStartTimingDetailLocked(targetSlot, openedAt)) return } @@ -586,7 +596,10 @@ func (l *LeaderLoop) productionWindowDeadlineLocked(slot uint64) time.Time { if deadline.IsZero() { return time.Time{} } - reserve := leaderBlockCompletionReserve + reserve := l.completionReserve + if reserve <= 0 { + reserve = leaderBlockCompletionReserve + } if reserve >= l.slotDuration { reserve = l.slotDuration / 4 } @@ -1205,12 +1218,16 @@ func (l *LeaderLoop) startSlotLocked(slot uint64) error { if startEntryHash == (solana.Hash{}) { startEntryHash = parentCtx.ParentBankhash } + limits, err := costmodel.LimitsForSlot(slotCtx.Features, epochSchedule, slot) + if err != nil { + return fmt.Errorf("leader slot limits: %w", err) + } sink := NewShredSink(session) bank := NewWorkingBank(BankConfig{ SlotCtx: slotCtx, Slot: slot, Leader: l.identity.PublicKey(), - Limits: costmodel.LimitsForFeatures(slotCtx.Features), + Limits: limits, EntryHash: startEntryHash, Sink: sink, TransactionStatuses: parentCtx.TransactionStatuses, diff --git a/pkg/blockprod/leader_completion_reserve_test.go b/pkg/blockprod/leader_completion_reserve_test.go new file mode 100644 index 000000000..062bf84d4 --- /dev/null +++ b/pkg/blockprod/leader_completion_reserve_test.go @@ -0,0 +1,32 @@ +package blockprod + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestConfiguredCompletionReserveKeepsProtocolDeadlines(t *testing.T) { + ready := time.Unix(1_700_000_000, 0) + now := ready + loop := NewLeaderLoop(LeaderLoopConfig{ + SlotDuration: AlpenglowSlotDuration, CompletionReserve: 60 * time.Millisecond, + Now: func() time.Time { return now }, + }) + loop.productionWindow = leaderProductionWindow{ + active: true, startSlot: 212, endSlot: 215, nextSlot: 212, readyAt: ready, + } + for offset := uint64(0); offset < 4; offset++ { + slot := 212 + offset + deadline := ready.Add(time.Duration(offset+1) * 200 * time.Millisecond) + require.Equal(t, deadline, loop.productionWindowProtocolDeadlineLocked(slot)) + require.Equal(t, deadline.Add(-60*time.Millisecond), loop.productionWindowDeadlineLocked(slot)) + // The override admits starts during the additional 15ms, but still + // rejects at its own cutoff instead of extending the protocol deadline. + now = deadline.Add(-70 * time.Millisecond) + require.NoError(t, loop.productionStartCutoffErrorLocked(slot)) + now = deadline.Add(-60 * time.Millisecond) + require.ErrorIs(t, loop.productionStartCutoffErrorLocked(slot), errProductionStartCutoffElapsed) + } +} diff --git a/pkg/blockprod/leader_processing_test.go b/pkg/blockprod/leader_processing_test.go new file mode 100644 index 000000000..71303a2cf --- /dev/null +++ b/pkg/blockprod/leader_processing_test.go @@ -0,0 +1,81 @@ +package blockprod + +import ( + "sync/atomic" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/arena" + "github.com/Overclock-Validator/mithril/pkg/metrics" + "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" + "github.com/stretchr/testify/require" +) + +func TestLeaderExecutionOptionsPreserveResults(t *testing.T) { + for _, testCase := range []string{"success", "instruction_failure", "payer_failure"} { + t.Run(testCase, func(t *testing.T) { + env := NewTestEnv(TestEnvConfig{}) + defer env.Close() + tx, err := solana.TransactionFromBytes(txfixture.MustSignedTransferWire(42)) + require.NoError(t, err) + if testCase == "instruction_failure" { + tx.Message.Instructions[0].Data[0] = 0xff + } + if testCase == "payer_failure" { + setPayerLamports(t, env, 1) + } + input := replay.LoadAndExecuteTransactionInput{SlotCtx: env.SlotCtx, Transaction: tx, LeanResult: true} + reference := replay.LoadAndExecuteTransaction(input) + input.SkipTimingMetrics = true + // A one-object arena also exercises the heap fallback. Repeated calls + // reset it, while the previous result's account state remains valid. + input.Arena = arena.New[sealevel.BorrowedAccount](1) + before := atomic.LoadUint64(&metrics.GlobalBlockReplay.InstructionsAndAccountMetasFromTx.Count) + beforeDispatch := atomic.LoadUint64(&metrics.GlobalBlockReplay.GetNextIxCtx.Count) + for i := 0; i < 3; i++ { + got := replay.LoadAndExecuteTransaction(input) + require.Equal(t, reference.ProcessingResult, got.ProcessingResult) + require.Equal(t, reference.FeeInfo, got.FeeInfo) + require.Equal(t, reference.LoadedAccountsDataSize, got.LoadedAccountsDataSize) + if reference.ExecCtx != nil { + require.NotNil(t, got.ExecCtx) + require.Equal(t, reference.ExecCtx.ComputeMeter.Used(), got.ExecCtx.ComputeMeter.Used()) + require.Equal(t, reference.ExecCtx.TransactionContext.Accounts.Accounts, got.ExecCtx.TransactionContext.Accounts.Accounts) + } + } + require.Equal(t, before, atomic.LoadUint64(&metrics.GlobalBlockReplay.InstructionsAndAccountMetasFromTx.Count)) + require.Equal(t, beforeDispatch, atomic.LoadUint64(&metrics.GlobalBlockReplay.GetNextIxCtx.Count)) + }) + } +} + +func TestWorkingBankReusesBorrowedAccountsWithoutChangingMessages(t *testing.T) { + env := NewTestEnv(TestEnvConfig{}) + defer env.Close() + payerBefore, err := env.SlotCtx.GetAccount(txfixture.PayerPubkey()) + require.NoError(t, err) + payerBalance := payerBefore.Lamports + destBefore, err := env.SlotCtx.GetAccount(txfixture.DestPubkey()) + require.NoError(t, err) + destBalance := destBefore.Lamports + const count = 130 + for i := 0; i < count; i++ { + wire := txfixture.MustSignedTransferWire(uint64(i)) + tx, err := solana.TransactionFromBytes(wire) + require.NoError(t, err) + result, _ := env.Bank.ForgeTransaction(tx, len(wire)) + require.Equal(t, ForgeAccepted, result) + after, err := tx.MarshalBinary() + require.NoError(t, err) + require.Equal(t, wire, after) + } + payerAfter, err := env.SlotCtx.GetAccount(txfixture.PayerPubkey()) + require.NoError(t, err) + destAfter, err := env.SlotCtx.GetAccount(txfixture.DestPubkey()) + require.NoError(t, err) + const transferred = count * (count + 1) / 2 + require.Equal(t, payerBalance-transferred-count*5000, payerAfter.Lamports) + require.Equal(t, destBalance+transferred, destAfter.Lamports) +} diff --git a/pkg/blockprod/leader_throughput_bench_test.go b/pkg/blockprod/leader_throughput_bench_test.go new file mode 100644 index 000000000..006633c93 --- /dev/null +++ b/pkg/blockprod/leader_throughput_bench_test.go @@ -0,0 +1,117 @@ +package blockprod + +import ( + "math" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/Overclock-Validator/mithril/pkg/addresses" + "github.com/Overclock-Validator/mithril/pkg/costmodel" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/replay" + "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" + "github.com/gagliardetto/solana-go" + computebudget "github.com/gagliardetto/solana-go/programs/compute-budget" +) + +// BenchmarkWorkingBankHotAccounts measures admission, execution, publication, +// and entry batching. Signing and bank setup are outside the measured region. +// All wires are unique within a bank, so this cannot benchmark dedup rejection. +func BenchmarkWorkingBankHotAccounts(b *testing.B) { benchmarkWorkingBankHotAccounts(b, "wire") } +func BenchmarkWorkingBankDecodedHotAccounts(b *testing.B) { + benchmarkWorkingBankHotAccounts(b, "decoded") +} +func BenchmarkWorkingBankPreparedHotAccounts(b *testing.B) { + benchmarkWorkingBankHotAccounts(b, "prepared") +} + +func benchmarkWorkingBankHotAccounts(b *testing.B, mode string) { + const perBank = 10000 + for _, workload := range []string{"compute_budget", "transfer"} { + b.Run(workload, func(b *testing.B) { + wires := make([][]byte, perBank) + for i := range wires { + if workload == "transfer" { + wires[i] = txfixture.MustSignedTransferWire(uint64(i)) + continue + } + tx, err := solana.NewTransaction([]solana.Instruction{ + computebudget.NewSetComputeUnitLimitInstruction(uint32(1000 + i)).Build(), + }, txfixture.TestBlockhash(), solana.TransactionPayer(txfixture.PayerPubkey())) + if err != nil { + b.Fatal(err) + } + key := txfixture.PayerPrivateKey() + if _, err = tx.Sign(func(solana.PublicKey) *solana.PrivateKey { return &key }); err != nil { + b.Fatal(err) + } + wires[i], err = tx.MarshalBinary() + if err != nil { + b.Fatal(err) + } + } + decoded := make([]*solana.Transaction, perBank) + for i, wire := range wires { + var err error + decoded[i], err = solana.TransactionFromBytes(wire) + if err != nil { + b.Fatal(err) + } + } + var prepared []*replay.PreparedTransaction + var env *TestEnv + defer func() { + if env != nil { + env.Close() + } + }() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if i%perBank == 0 { + b.StopTimer() + if env != nil { + env.Close() + } + env = NewTestEnv(TestEnvConfig{}) + env.SlotCtx.Features.EnableFeature(features.RemoveAccountsDeltaHash, 0) + env.SlotCtx.Features.EnableFeature(features.RaiseBlockLimitsTo100m, 0) + if err := env.SlotCtx.Accounts.SetAccount(&addresses.ComputeBudgetProgramAddr, &accounts.Account{ + Key: addresses.ComputeBudgetProgramAddr, Lamports: 1, + Owner: addresses.NativeLoaderAddr, Executable: true, RentEpoch: math.MaxUint64, + }); err != nil { + b.Fatal(err) + } + + if mode == "prepared" { + // Bind the immutable snapshot after all fixture feature setup is complete. + env.Bank.preparer = replay.NewTransactionPreparer(env.SlotCtx.Features) + if prepared == nil { + prepared = make([]*replay.PreparedTransaction, perBank) + for j, tx := range decoded { + prepared[j] = env.Bank.preparer.Prepare(tx) + if prepared[j] == nil { + b.Fatal("preparation failed") + } + } + } + } + b.StartTimer() + } + var result ForgeResult + var reason costmodel.ExceedReason + switch mode { + case "prepared": + result, reason = env.Bank.ForgePreparedTransaction(decoded[i%perBank], len(wires[i%perBank]), prepared[i%perBank]) + case "decoded": + result, reason = env.Bank.ForgeTransaction(decoded[i%perBank], len(wires[i%perBank])) + default: + result, reason = env.Bank.Forge(wires[i%perBank]) + } + if result != ForgeAccepted { + b.Fatalf("transaction %d: %v / %v", i, result, reason) + } + } + }) + } +} diff --git a/pkg/blockprod/prepared_transaction_test.go b/pkg/blockprod/prepared_transaction_test.go new file mode 100644 index 000000000..9a6153d64 --- /dev/null +++ b/pkg/blockprod/prepared_transaction_test.go @@ -0,0 +1,142 @@ +package blockprod + +import ( + "testing" + + "github.com/Overclock-Validator/mithril/pkg/costmodel" + "github.com/Overclock-Validator/mithril/pkg/features" + "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/require" +) + +func TestPreparedBankPreservesOutcomes(t *testing.T) { + for _, kind := range []string{"success", "instruction_failure", "fees_only", "payer_changed", "expired", "duplicate", "foreign_message"} { + t.Run(kind, func(t *testing.T) { + reference := NewTestEnv(TestEnvConfig{}) + defer reference.Close() + candidate := NewTestEnv(TestEnvConfig{}) + defer candidate.Close() + tx := mustSignedTransfer(t, 7) + if kind == "instruction_failure" { + tx.Message.Instructions[0].Data[0] = 0xff + } + if kind == "fees_only" { + tx = mustSignBankTestTransaction(t, + computebudget.NewSetLoadedAccountsDataSizeLimitInstruction(1).Build(), + system.NewTransferInstruction(1, txfixture.PayerPubkey(), txfixture.DestPubkey()).Build()) + } + if kind == "expired" { + tx.Message.RecentBlockhash = solana.Hash{99} + } + prepared := replay.NewTransactionPreparer(candidate.SlotCtx.Features.Clone()).Prepare(tx) + require.NotNil(t, prepared) + estimate, err := costmodel.EstimateTransactionCost(tx, candidate.SlotCtx.Features) + require.NoError(t, err) + require.Equal(t, estimate, prepared.Cost()) + if kind == "payer_changed" { + // Preparation succeeded while the payer was funded. Admission must + // still reject after another transaction spends that balance. + setPayerLamports(t, reference, 1) + setPayerLamports(t, candidate, 1) + } + if kind == "foreign_message" { + tx = mustSignedTransfer(t, 19) + } + wire, err := tx.MarshalBinary() + require.NoError(t, err) + for i := 0; i < 2; i++ { + a, ar := reference.Bank.ForgeTransaction(tx, len(wire)) + b, br := candidate.Bank.ForgePreparedTransaction(tx, len(wire), prepared) + require.Equal(t, a, b) + require.Equal(t, ar, br) + require.Equal(t, reference.Bank.CostTracker().BlockCost(), candidate.Bank.CostTracker().BlockCost()) + require.Equal(t, reference.Bank.TxFeeAccumulator(), candidate.Bank.TxFeeAccumulator()) + for _, key := range []solana.PublicKey{txfixture.PayerPubkey(), txfixture.DestPubkey()} { + ra, err := reference.SlotCtx.GetAccount(key) + require.NoError(t, err) + ca, err := candidate.SlotCtx.GetAccount(key) + require.NoError(t, err) + require.Equal(t, ra, ca) + } + if kind != "duplicate" { + break + } + } + after, err := tx.MarshalBinary() + require.NoError(t, err) + require.Equal(t, wire, after) + }) + } +} + +func TestPreparedBankRejectsStaleFeatures(t *testing.T) { + env := NewTestEnv(TestEnvConfig{}) + defer env.Close() + future := env.SlotCtx.Features.Clone() + future.EnableFeature(features.EnableTxV1, 0) + tx := mustSignedTransfer(t, 1) + _, err := tx.Message.SetVersion(solana.MessageVersionV1) + require.NoError(t, err) + prepared := replay.NewTransactionPreparer(future).Prepare(tx) + require.NotNil(t, prepared) + require.False(t, env.Bank.preparer.Matches(prepared, tx, env.SlotCtx.Features)) + wire, err := tx.MarshalBinary() + require.NoError(t, err) + result, _ := env.Bank.ForgePreparedTransaction(tx, len(wire), prepared) + require.Equal(t, ForgeDroppedExecution, result) + require.Empty(t, env.Bank.ForgedTransactions()) + require.Zero(t, env.Bank.TxFeeAccumulator().TotalFees) +} + +func TestPreparedLeaderStillRejectsFeePayerNoOp(t *testing.T) { + env := NewTestEnv(TestEnvConfig{}) + defer env.Close() + // Establish the complete feature snapshot before constructing the bank. + env.SlotCtx.Features.EnableFeature(features.RelaxFeePayerConstraint, 0) + bank := NewWorkingBank(BankConfig{SlotCtx: env.SlotCtx, Slot: env.SlotCtx.Slot, + TransactionStatuses: replay.NewTransactionStatusCache().View()}) + tx := mustSignedTransfer(t, 1) + prepared := bank.preparer.Prepare(tx) + require.NotNil(t, prepared) + setPayerLamports(t, env, 1) + preview := bank.preparer.LoadAndExecute(replay.LoadAndExecuteTransactionInput{ + SlotCtx: env.SlotCtx, Transaction: tx, LeanResult: true}, prepared) + require.True(t, preview.ProcessedAsNoOp) + wire, err := tx.MarshalBinary() + require.NoError(t, err) + result, _ := bank.ForgePreparedTransaction(tx, len(wire), prepared) + require.Equal(t, ForgeDroppedExecution, result) + require.Empty(t, bank.ForgedTransactions()) + require.Zero(t, bank.TxFeeAccumulator().TotalFees) +} + +func TestPreparedExecutionRechecksStateWithoutMutatingPreparation(t *testing.T) { + env := NewTestEnv(TestEnvConfig{}) + defer env.Close() + tx := mustSignedTransfer(t, 1) + p := replay.NewTransactionPreparer(env.SlotCtx.Features) + prepared := p.Prepare(tx) + require.NotNil(t, prepared) + input := replay.LoadAndExecuteTransactionInput{SlotCtx: env.SlotCtx, Transaction: tx, LeanResult: true} + for _, balance := range []uint64{10_000_000, 1, 20_000_000} { + setPayerLamports(t, env, balance) + got := p.LoadAndExecute(input, prepared) + want := replay.LoadAndExecuteTransaction(input) + require.Equal(t, want.ProcessingResult, got.ProcessingResult) + require.Equal(t, want.FeeInfo, got.FeeInfo) + require.Equal(t, want.LoadedAccountsDataSize, got.LoadedAccountsDataSize) + if want.ExecCtx != nil { + require.Equal(t, want.ExecCtx.TransactionContext.Accounts.Accounts, got.ExecCtx.TransactionContext.Accounts.Accounts) + require.Equal(t, want.ExecCtx.ComputeMeter.Used(), got.ExecCtx.ComputeMeter.Used()) + } + } + // Rent-boundary checks still use the bank's current payer state. + rent := sealevel.NewDefaultRentSysvar() + setPayerLamports(t, env, rent.MinimumBalance(0)+4999) + require.Error(t, p.PayerCanFund(env.SlotCtx, tx, prepared)) +} diff --git a/pkg/blockprod/readonly_block_bench_test.go b/pkg/blockprod/readonly_block_bench_test.go new file mode 100644 index 000000000..b3b8ed713 --- /dev/null +++ b/pkg/blockprod/readonly_block_bench_test.go @@ -0,0 +1,187 @@ +package blockprod + +import ( + "crypto/ed25519" + "crypto/sha256" + "math" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/Overclock-Validator/mithril/pkg/addresses" + "github.com/Overclock-Validator/mithril/pkg/costmodel" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" + "github.com/Overclock-Validator/mithril/pkg/turbine" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +const readonlyBlockAccepted = 48622 // 50M budget, including the next tx's upfront loaded-data reservation. +const readonlyActualCost = 1028 + +type readonlyBlockParent struct{ mem accounts.MemAccounts } + +func (p readonlyBlockParent) GetAccount(_ uint64, key solana.PublicKey) (*accounts.Account, error) { + raw := [32]byte(key) + return p.mem.GetAccount(&raw) +} + +type readonlyBlockFixture struct { + wires [][]byte + txs []*solana.Transaction + payers []solana.PublicKey + parent readonlyBlockParent +} + +func makeReadonlyBlockFixture(tb testing.TB, count int) readonlyBlockFixture { + tb.Helper() + f := readonlyBlockFixture{parent: readonlyBlockParent{accounts.NewMemAccounts()}} + keys := make([]ed25519.PrivateKey, 8) + for i := range keys { + seed := sha256.Sum256([]byte{byte(i), 73}) + keys[i] = ed25519.NewKeyFromSeed(seed[:]) + f.payers = append(f.payers, solana.PublicKeyFromBytes(keys[i].Public().(ed25519.PublicKey))) + } + pool := make([]solana.PublicKey, txfixture.ReadonlyPairPoolSize) + for i := range pool { + pool[i] = solana.PublicKey{byte(i + 1), 77} + require.NoError(tb, f.parent.mem.SetAccountWithoutLock(pool[i], &accounts.Account{ + Key: pool[i], Lamports: 1_000_000, Data: make([]byte, 9), Owner: solana.PublicKey{11}, RentEpoch: math.MaxUint64, + })) + } + for i := 0; i < count; i++ { + wire, err := txfixture.ReadonlyPairWire(keys[i%8], txfixture.TestBlockhash(), pool, i/8) + require.NoError(tb, err) + tx, err := solana.TransactionFromBytes(wire) + require.NoError(tb, err) + f.wires = append(f.wires, wire) + f.txs = append(f.txs, tx) + } + return f +} + +func (f readonlyBlockFixture) bank(tb testing.TB, sink BatchSink) *TestEnv { + tb.Helper() + // Explicit active 200ms testnet budgets allow identical baseline/candidate + // benchmark fixtures; LimitsForSlot has separate epoch-transition tests. + limits := costmodel.DefaultLimits() + limits.BlockCost, limits.WritableAccountCost = 50_000_000, 20_000_000 + limits.AllocatedDataSizeDelta, limits.MaxEntryBytes = 50_000_000, 10*1024*1024-48 + env := NewTestEnv(TestEnvConfig{Limits: limits, Sink: sink}) + env.SlotCtx.Features.EnableFeature(features.RemoveAccountsDeltaHash, 0) + env.SlotCtx.UnrootedRead = f.parent + for _, payer := range f.payers { + require.NoError(tb, env.SlotCtx.Accounts.SetAccountWithoutLock(payer, &accounts.Account{ + Key: payer, Lamports: 10_000_000_000, Owner: addresses.SystemProgramAddr, RentEpoch: math.MaxUint64, + })) + } + return env +} + +// This is a capacity/correctness test, not a 200ms deadline assertion. It creates +// a full synthetic block, validates fee/cost accounting, and round-trips every +// emitted entry through the production shred generator and decoder. No network. +func TestReadonlyPairBlockCapacityAndShredRoundTrip(t *testing.T) { + f := makeReadonlyBlockFixture(t, readonlyBlockAccepted+1) + sink := &captureSink{} + env := f.bank(t, sink) + defer env.Close() + for i, tx := range f.txs { + result, reason := env.Bank.ForgeTransaction(tx, len(f.wires[i])) + if i < readonlyBlockAccepted { + require.Equal(t, ForgeAccepted, result, "transaction %d", i) + } else { + require.Equal(t, ForgeDroppedCost, result) + require.Equal(t, costmodel.ExceedBlockCost, reason) + } + } + env.Bank.Freeze() + require.Equal(t, uint64(readonlyBlockAccepted*readonlyActualCost), env.Bank.CostTracker().BlockCost()) + require.Equal(t, uint64(readonlyBlockAccepted), env.Bank.NumSignatures()) + require.Equal(t, uint64(readonlyBlockAccepted*5000), env.Bank.TxFeeAccumulator().TotalFees) + require.Len(t, env.Bank.ForgedTransactions(), readonlyBlockAccepted) + require.LessOrEqual(t, uint64(env.Bank.EntryBytes()), env.Bank.CostTracker().Limits().MaxEntryBytes) + for i, payer := range f.payers { + included := readonlyBlockAccepted / 8 + if i < readonlyBlockAccepted%8 { + included++ + } + acct, err := env.SlotCtx.GetAccount(payer) + require.NoError(t, err) + require.Equal(t, uint64(10_000_000_000-included*5000), acct.Lamports) + } + gen := turbine.ShredGenerator{Slot: 42, ParentSlot: 41, Version: 7} + var root solana.Hash + var nextData, nextCode uint32 + count, totalBytes := 0, 0 + previous := solana.Hash{0xab} + for i, entries := range sink.batches { + raw, err := marshalEntryBatchBytes(entries) + require.NoError(t, err) + require.Equal(t, len(raw), sink.bytes[i]) + totalBytes += len(raw) + packets, chained, d, c, err := gen.MakeShredsFromData(txfixture.PayerPrivateKey(), raw, false, root, nextData, nextCode) + require.NoError(t, err) + root, nextData, nextCode = chained, d, c + var shreds []*turbine.Shred + for _, packet := range packets { + sh, err := turbine.ParseShred(packet) + require.NoError(t, err) + if sh.Type == turbine.ShredTypeData { + shreds = append(shreds, sh) + } + } + decoded, err := turbine.DecodeEntriesFromDataShreds(shreds) + require.NoError(t, err) + require.Equal(t, entries, decoded) + for _, entry := range decoded { + require.Equal(t, turbine.NextAlpenglowEntryHash(previous, entry.NumHashes, entry.Txns), entry.Hash) + previous = entry.Hash + for _, tx := range entry.Txns { + require.Equal(t, f.txs[count].Signatures, tx.Signatures) + count++ + } + } + } + require.Equal(t, readonlyBlockAccepted, count) + require.Equal(t, env.Bank.EntryBytes(), totalBytes) + require.Equal(t, env.Bank.EntryHash(), previous) + require.Less(t, nextData, uint32(16384)) // Leaves room for header/footer/ending tick. +} + +// One serial caller; signing and fixture/bank setup are excluded. Measures +// admission, execution, account publication, entry building and final flush. +// It excludes signature verification, actual AccountsDB, network and consensus. +func BenchmarkReadonlyPairFullBlock(b *testing.B) { + f := makeReadonlyBlockFixture(b, readonlyBlockAccepted) + for _, mode := range []string{"wire", "decoded"} { + b.Run(mode, func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + env := f.bank(b, nil) + b.StartTimer() + for j, wire := range f.wires { + var result ForgeResult + if mode == "wire" { + result, _ = env.Bank.Forge(wire) + } else { + result, _ = env.Bank.ForgeTransaction(f.txs[j], len(wire)) + } + if result != ForgeAccepted { + b.Fatalf("transaction %d: %v", j, result) + } + } + env.Bank.Freeze() + b.StopTimer() + if env.Bank.CostTracker().BlockCost() != readonlyBlockAccepted*readonlyActualCost { + b.Fatal("unexpected block cost") + } + env.Close() + b.StartTimer() + } + b.ReportMetric(float64(readonlyBlockAccepted), "tx/block") + }) + } +} diff --git a/pkg/blockprod/readonly_block_prepared_bench_test.go b/pkg/blockprod/readonly_block_prepared_bench_test.go new file mode 100644 index 000000000..68957fcef --- /dev/null +++ b/pkg/blockprod/readonly_block_prepared_bench_test.go @@ -0,0 +1,45 @@ +package blockprod + +import ( + "testing" + + "github.com/Overclock-Validator/mithril/pkg/replay" +) + +// Models a queue already statically prepared before leadership. Preparation is +// shifted out of this timed bank phase, not eliminated from validator CPU work. +func BenchmarkReadonlyPairPreparedFullBlock(b *testing.B) { + f := makeReadonlyBlockFixture(b, readonlyBlockAccepted) + setup := f.bank(b, nil) + preparer := replay.NewTransactionPreparer(setup.SlotCtx.Features) + prepared := make([]*replay.PreparedTransaction, len(f.txs)) + for i, tx := range f.txs { + prepared[i] = preparer.Prepare(tx) + if prepared[i] == nil { + b.Fatal("preparation failed") + } + } + setup.Close() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + env := f.bank(b, nil) + env.Bank.preparer = replay.NewTransactionPreparer(env.SlotCtx.Features) + b.StartTimer() + for j, tx := range f.txs { + result, _ := env.Bank.ForgePreparedTransaction(tx, len(f.wires[j]), prepared[j]) + if result != ForgeAccepted { + b.Fatalf("transaction %d: %v", j, result) + } + } + env.Bank.Freeze() + b.StopTimer() + if env.Bank.CostTracker().BlockCost() != readonlyBlockAccepted*readonlyActualCost { + b.Fatal("unexpected block cost") + } + env.Close() + b.StartTimer() + } + b.ReportMetric(float64(readonlyBlockAccepted), "tx/block") +} diff --git a/pkg/blockprod/readonly_load_bench_test.go b/pkg/blockprod/readonly_load_bench_test.go new file mode 100644 index 000000000..f5d24798d --- /dev/null +++ b/pkg/blockprod/readonly_load_bench_test.go @@ -0,0 +1,95 @@ +package blockprod + +import ( + "crypto/ed25519" + "math" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/accounts" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/replay" + "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" + "github.com/gagliardetto/solana-go" +) + +type readonlyBenchParent struct{ mem accounts.MemAccounts } + +func (p readonlyBenchParent) GetAccount(_ uint64, key solana.PublicKey) (*accounts.Account, error) { + raw := [32]byte(key) + return p.mem.GetAccount(&raw) +} + +// Only bank admission/execution/entry building are timed. The immutable parent +// is in memory; this excludes actual AccountsDB, network and signing costs. +func BenchmarkReadonlyPairBank(b *testing.B) { + const perBank = 10000 + parent := readonlyBenchParent{accounts.NewMemAccounts()} + pool := make([]solana.PublicKey, 128) + for i := range pool { + pool[i] = solana.PublicKey{byte(i + 1), 77} + parent.mem.SetAccountWithoutLock(pool[i], &accounts.Account{Key: pool[i], Lamports: 1_000_000, Data: make([]byte, 9), Owner: solana.PublicKey{11}, RentEpoch: math.MaxUint64}) + } + txs := make([]*solana.Transaction, perBank) + key := txfixture.PayerPrivateKey() + payer := key.PublicKey() + hash := txfixture.TestBlockhash() + for i := range txs { + n := (i * 7919) % (128 * 127) + a, c := n/127, n%127 + if c >= a { + c++ + } + msg := []byte{1, 0, 2, 3} + msg = append(msg, payer[:]...) + msg = append(msg, pool[a][:]...) + msg = append(msg, pool[c][:]...) + msg = append(msg, hash[:]...) + msg = append(msg, 0) + wire := []byte{1} + wire = append(wire, ed25519.Sign(ed25519.PrivateKey(key), msg)...) + wire = append(wire, msg...) + var err error + txs[i], err = solana.TransactionFromBytes(wire) + if err != nil { + b.Fatal(err) + } + } + var env *TestEnv + var prepared []*replay.PreparedTransaction + defer func() { + if env != nil { + env.Close() + } + }() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if i%perBank == 0 { + b.StopTimer() + if env != nil { + env.Close() + } + env = NewTestEnv(TestEnvConfig{}) + env.SlotCtx.Features.EnableFeature(features.RemoveAccountsDeltaHash, 0) + env.SlotCtx.UnrootedRead = parent + env.Bank.preparer = replay.NewTransactionPreparer(env.SlotCtx.Features) + if prepared == nil { + prepared = make([]*replay.PreparedTransaction, perBank) + for j, t := range txs { + prepared[j] = env.Bank.preparer.Prepare(t) + if prepared[j] == nil { + b.Fatal("preparation failed") + } + } + } + b.StartTimer() + } + outcome, reason := env.Bank.ForgePreparedTransaction(txs[i%perBank], 198, prepared[i%perBank]) + if outcome != ForgeAccepted { + b.Fatalf("%v / %v", outcome, reason) + } + if i%perBank == perBank-1 && env.Bank.CostTracker().BlockCost() != perBank*1028 { + b.Fatalf("unexpected cost %d", env.Bank.CostTracker().BlockCost()) + } + } +} diff --git a/pkg/blockprod/scheduler/buffer.go b/pkg/blockprod/scheduler/buffer.go index 307dc0b28..bb493529c 100644 --- a/pkg/blockprod/scheduler/buffer.go +++ b/pkg/blockprod/scheduler/buffer.go @@ -4,15 +4,17 @@ import ( "container/heap" "sync" + "github.com/Overclock-Validator/mithril/pkg/replay" "github.com/gagliardetto/solana-go" ) -// MaxBufferedTxns is the hard cap on cross-slot buffered transactions. +// MaxBufferedTxns is the default cap on cross-slot buffered transactions. const MaxBufferedTxns = 2 * 65536 // entry is one buffered, scored transaction. type entry struct { - tx *solana.Transaction + tx *solana.Transaction + prepared *replay.PreparedTransaction // wire is an owned copy of the packet bytes. Parsed tx fields may alias it // (solana-go decoder slices), so it must outlive any use of tx. wire []byte @@ -25,28 +27,20 @@ type entry struct { // (e.g. cost limit). The entry is retained for cross-slot retry. skipGen uint64 - alive bool - maxIdx int - minIdx int + alive bool } type maxHeap []*entry func (h maxHeap) Len() int { return len(h) } func (h maxHeap) Less(i, j int) bool { - if h[i].reward != h[j].reward { - return h[i].reward > h[j].reward - } - return h[i].seq < h[j].seq // older first on ties + return higherPriority(h[i], h[j]) } func (h maxHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] - h[i].maxIdx = i - h[j].maxIdx = j } func (h *maxHeap) Push(x any) { e := x.(*entry) - e.maxIdx = len(*h) *h = append(*h, e) } func (h *maxHeap) Pop() any { @@ -55,7 +49,6 @@ func (h *maxHeap) Pop() any { e := old[n-1] old[n-1] = nil *h = old[:n-1] - e.maxIdx = -1 return e } @@ -71,12 +64,9 @@ func (h minHeap) Less(i, j int) bool { } func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] - h[i].minIdx = i - h[j].minIdx = j } func (h *minHeap) Push(x any) { e := x.(*entry) - e.minIdx = len(*h) *h = append(*h, e) } func (h *minHeap) Pop() any { @@ -85,7 +75,6 @@ func (h *minHeap) Pop() any { e := old[n-1] old[n-1] = nil *h = old[:n-1] - e.minIdx = -1 return e } @@ -205,7 +194,7 @@ func (b *Buffer) killLocked(e *entry) { func (b *Buffer) popMaxAliveLocked() *entry { for b.max.Len() > 0 { - e := heap.Pop(&b.max).(*entry) + e := b.max.popEntry() if !e.alive { continue } @@ -215,6 +204,44 @@ func (b *Buffer) popMaxAliveLocked() *entry { return nil } +// popEntry moves the winning child into the hole at each level. This avoids +// interface dispatch and swapping two entries at every level of a large queue. +// Ordering is identical to maxHeap.Less, including FIFO for equal rewards. +func (h *maxHeap) popEntry() *entry { + nodes := *h + root := nodes[0] + last := nodes[len(nodes)-1] + nodes[len(nodes)-1] = nil + nodes = nodes[:len(nodes)-1] + if len(nodes) > 0 { + i := 0 + for { + child := 2*i + 1 + if child >= len(nodes) { + break + } + if child+1 < len(nodes) && higherPriority(nodes[child+1], nodes[child]) { + child++ + } + if !higherPriority(nodes[child], last) { + break + } + nodes[i] = nodes[child] + i = child + } + nodes[i] = last + } + *h = nodes + return root +} + +func higherPriority(a, b *entry) bool { + if a.reward != b.reward { + return a.reward > b.reward + } + return a.seq < b.seq +} + func (b *Buffer) peekMinAliveLocked() *entry { for b.min.Len() > 0 { if b.min[0].alive { diff --git a/pkg/blockprod/scheduler/buffer_bench_test.go b/pkg/blockprod/scheduler/buffer_bench_test.go new file mode 100644 index 000000000..ccdaa3af0 --- /dev/null +++ b/pkg/blockprod/scheduler/buffer_bench_test.go @@ -0,0 +1,45 @@ +package scheduler + +import ( + "encoding/binary" + "testing" + + "github.com/gagliardetto/solana-go" +) + +// BenchmarkBufferDrain measures selection from a large, already-filled TPU +// queue. Transaction decoding and queue filling are outside the timed region. +func BenchmarkBufferDrain(b *testing.B) { + const count = 120000 + for _, mixed := range []bool{false, true} { + name := "equal_rewards" + if mixed { + name = "mixed_rewards" + } + b.Run(name, func(b *testing.B) { + var buffer *Buffer + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if i%count == 0 { + b.StopTimer() + buffer = NewBuffer(count) + for j := 0; j < count; j++ { + e := &entry{tx: &solana.Transaction{}, seq: uint64(j), reward: 2500} + binary.LittleEndian.PutUint64(e.messageHash[:], uint64(j)) + if mixed { + e.reward = uint64(j*7919) % 1000 + } + if result, _ := buffer.Insert(e); result != InsertAccepted { + b.Fatal("queue fill failed") + } + } + b.StartTimer() + } + if buffer.PopMax() == nil { + b.Fatal("queue drained prematurely") + } + } + }) + } +} diff --git a/pkg/blockprod/scheduler/buffer_order_test.go b/pkg/blockprod/scheduler/buffer_order_test.go new file mode 100644 index 000000000..7ed342c33 --- /dev/null +++ b/pkg/blockprod/scheduler/buffer_order_test.go @@ -0,0 +1,105 @@ +package scheduler + +import ( + "container/heap" + "encoding/binary" + "math/rand" + "sort" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func TestMaxHeapRemovalMatchesSortedOrder(t *testing.T) { + rng := rand.New(rand.NewSource(417)) + for _, count := range []int{1, 2, 3, 4, 7, 8, 9, 127, 128, 129, 4095, 4096, 4097} { + var h maxHeap + want := make([]*entry, count) + for i := range want { + want[i] = &entry{seq: uint64(i), reward: uint64(rng.Intn(17))} + heap.Push(&h, want[i]) + } + sort.Slice(want, func(i, j int) bool { + if want[i].reward == want[j].reward { + return want[i].seq < want[j].seq + } + return want[i].reward > want[j].reward + }) + backing := h[:cap(h)] + for i, expected := range want { + require.Same(t, expected, h.popEntry(), "count=%d pop=%d", count, i) + require.Nil(t, backing[len(h)], "removed pointer retained") + } + } +} + +// Compare interleaved insertion, eviction, cleanup and removal with a small +// unsorted reference model. Reusing hashes after removal also exercises stale +// nodes left in the other heap without reusing the entry objects themselves. +func TestBufferMixedOperationsMatchReference(t *testing.T) { + const capacity = 64 + rng := rand.New(rand.NewSource(418)) + b := NewBuffer(capacity) + model := make(map[[32]byte]*entry) + best := func(high bool) *entry { + var found *entry + for _, e := range model { + if found == nil || (high && (e.reward > found.reward || e.reward == found.reward && e.seq < found.seq)) || + (!high && (e.reward < found.reward || e.reward == found.reward && e.seq > found.seq)) { + found = e + } + } + return found + } + for step := 0; step < 10000; step++ { + switch action := rng.Intn(10); { + case action < 7: + e := &entry{tx: &solana.Transaction{}, seq: uint64(step), reward: uint64(rng.Intn(16))} + binary.LittleEndian.PutUint64(e.messageHash[:], uint64(rng.Intn(256))) + wantResult := InsertAccepted + var wantEvicted *entry + if _, duplicate := model[e.messageHash]; duplicate { + wantResult = InsertDuplicate + } else if len(model) == capacity { + lowest := best(false) + if e.reward <= lowest.reward { + wantResult = InsertRejectedCapacity + } else { + wantEvicted = lowest + delete(model, lowest.messageHash) + } + } + if wantResult == InsertAccepted { + model[e.messageHash] = e + } + got, evicted := b.Insert(e) + require.Equal(t, wantResult, got, "step=%d", step) + require.True(t, wantEvicted == evicted, "eviction differs at step=%d", step) + case action < 9: + want := best(true) + got := b.PopMax() + require.True(t, want == got, "selection differs at step=%d", step) + if want != nil { + delete(model, want.messageHash) + } + default: + mod := uint64(rng.Intn(11)) + want := 0 + for hash, e := range model { + if e.seq%11 == mod { + delete(model, hash) + want++ + } + } + require.Equal(t, want, b.Cleanup(func(e *entry) bool { return e.seq%11 == mod })) + } + require.Equal(t, len(model), b.Len(), "step=%d", step) + } + for len(model) > 0 { + want := best(true) + require.Same(t, want, b.PopMax()) + delete(model, want.messageHash) + } + require.Nil(t, b.PopMax()) +} diff --git a/pkg/blockprod/scheduler/config_test.go b/pkg/blockprod/scheduler/config_test.go new file mode 100644 index 000000000..9f43ed511 --- /dev/null +++ b/pkg/blockprod/scheduler/config_test.go @@ -0,0 +1,28 @@ +package scheduler + +import ( + "testing" + + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/stretchr/testify/require" +) + +func TestConfiguredQueueCapacityAndPreparation(t *testing.T) { + defaults := NewWithConfig(nil, Config{}) + require.Equal(t, MaxBufferedTxns, defaults.buffer.Capacity()) + feats := features.NewFeaturesDefault() + custom := NewWithConfig(nil, Config{MaxBufferedTransactions: 2, FeatureSource: func() *features.Features { return feats }}) + require.Equal(t, 2, custom.buffer.Capacity()) + require.NotNil(t, custom.preparer.Load()) + for i := byte(1); i <= 2; i++ { + result, _ := custom.buffer.Insert(testEntry(10, uint64(i), i)) + require.Equal(t, InsertAccepted, result) + } + result, evicted := custom.buffer.Insert(testEntry(9, 3, 3)) + require.Equal(t, InsertRejectedCapacity, result) + require.Nil(t, evicted) + result, evicted = custom.buffer.Insert(testEntry(11, 4, 4)) + require.Equal(t, InsertAccepted, result) + require.Equal(t, uint64(2), evicted.seq) + require.Equal(t, 2, custom.Buffered()) +} diff --git a/pkg/blockprod/scheduler/scheduler.go b/pkg/blockprod/scheduler/scheduler.go index de6196f30..88e6435bf 100644 --- a/pkg/blockprod/scheduler/scheduler.go +++ b/pkg/blockprod/scheduler/scheduler.go @@ -9,6 +9,7 @@ import ( "github.com/Overclock-Validator/mithril/pkg/blockprod" "github.com/Overclock-Validator/mithril/pkg/costmodel" "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/replay" "github.com/Overclock-Validator/mithril/pkg/tpu/packet" "github.com/gagliardetto/solana-go" ) @@ -43,9 +44,12 @@ type Stats struct { // Scheduler buffers verified TPU transactions in a reward-ordered heap and // drains into the active WorkingBank when one is published. type Scheduler struct { - banks BankSource - feats *features.Features - buffer *Buffer + banks BankSource + feats *features.Features + buffer *Buffer + preparer atomic.Pointer[replay.TransactionPreparer] + featureSource func() *features.Features + lastPreparationRefresh time.Time seq atomic.Uint64 wake chan struct{} @@ -80,6 +84,37 @@ func New(banks BankSource) *Scheduler { } } +// NewWithFeatureSource prepares queued messages against a replay-tip snapshot. +// The active bank independently verifies feature compatibility before reuse. +func NewWithFeatureSource(banks BankSource, source func() *features.Features) *Scheduler { + return NewWithConfig(banks, Config{FeatureSource: source}) +} + +// Config controls the bounded TPU queue and static preparation source. +type Config struct { + // MaxBufferedTransactions defaults to MaxBufferedTxns when zero. + MaxBufferedTransactions int + FeatureSource func() *features.Features +} + +func NewWithConfig(banks BankSource, cfg Config) *Scheduler { + s := New(banks) + if cfg.MaxBufferedTransactions > 0 { + s.buffer = NewBuffer(cfg.MaxBufferedTransactions) + } + s.featureSource = cfg.FeatureSource + s.refreshPreparation() + return s +} + +func (s *Scheduler) refreshPreparation() { + if s.featureSource == nil || time.Since(s.lastPreparationRefresh) < 100*time.Millisecond { + return + } + s.preparer.Store(replay.NewTransactionPreparer(s.featureSource())) + s.lastPreparationRefresh = time.Now() +} + // Start launches the bank-gated drain loop. func (s *Scheduler) Start(ctx context.Context) { s.startOnce.Do(func() { @@ -138,6 +173,7 @@ func (s *Scheduler) Receive(pkt packet.Packet) { e := &entry{ tx: tx, + prepared: s.preparer.Load().Prepare(tx), wire: wire, wireSize: len(wire), messageHash: messageHash, @@ -219,6 +255,7 @@ func (s *Scheduler) drainLoop(ctx context.Context) { if ctx.Err() != nil { return } + s.refreshPreparation() bank := s.banks.WorkingBank() s.noteBank(bank) if bank == nil { @@ -258,15 +295,10 @@ func (s *Scheduler) drainLoop(ctx context.Context) { continue } - // Prefer the owned wire so forge reparses from stable bytes even if the - // retained tx view was somehow mutated after buffering. - var result blockprod.ForgeResult - var reason costmodel.ExceedReason - if len(e.wire) > 0 { - result, reason = bank.Forge(e.wire) - } else { - result, reason = bank.ForgeTransaction(e.tx, e.wireSize) - } + // Receive owns the wire and its decoded transaction for the entire queue + // lifetime. Execution modifies transaction-local account clones, not + // this immutable message, so reuse the decoded transaction across banks. + result, reason := bank.ForgePreparedTransaction(e.tx, e.wireSize, e.prepared) switch result { case blockprod.ForgeDroppedNoLeader: s.rebuffer(e) diff --git a/pkg/blockprod/scheduler/scheduler_test.go b/pkg/blockprod/scheduler/scheduler_test.go index e4b74e092..f83711312 100644 --- a/pkg/blockprod/scheduler/scheduler_test.go +++ b/pkg/blockprod/scheduler/scheduler_test.go @@ -7,6 +7,7 @@ import ( "github.com/Overclock-Validator/mithril/pkg/blockprod" "github.com/Overclock-Validator/mithril/pkg/costmodel" + "github.com/Overclock-Validator/mithril/pkg/features" "github.com/Overclock-Validator/mithril/pkg/fees" "github.com/Overclock-Validator/mithril/pkg/tpu/packet" "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" @@ -244,7 +245,9 @@ func TestMaxBufferedTxnsConstant(t *testing.T) { } func TestSchedulerCopiesPooledPacketBytes(t *testing.T) { - sched := New(blockprod.NewController()) + env := blockprod.NewTestEnv(blockprod.TestEnvConfig{}) + defer env.Close() + sched := NewWithFeatureSource(blockprod.NewController(), func() *features.Features { return env.SlotCtx.Features.Clone() }) pool := packet.NewPool(1) buf, idx, ok := pool.Acquire() require.True(t, ok) @@ -269,11 +272,26 @@ func TestSchedulerCopiesPooledPacketBytes(t *testing.T) { e := sched.buffer.PopMax() require.NotNil(t, e) require.Equal(t, wire, e.wire) + require.NotNil(t, e.prepared) // Re-parse and verify the buffered transaction still has a valid signature. tx, err := solana.TransactionFromBytes(e.wire) require.NoError(t, err) require.Equal(t, e.tx.Signatures[0], tx.Signatures[0]) + + // Exercise the actual drain path using the retained decoded transaction, + // after its original pooled packet has already been overwritten. + res, _ := sched.buffer.Insert(e) + require.Equal(t, InsertAccepted, res) + sched.banks = env.Controller + sched.Start(context.Background()) + defer sched.Stop() + require.Eventually(t, func() bool { return sched.Stats().Accepted == 1 }, time.Second, time.Millisecond) + forged := env.Bank.ForgedTransactions() + require.Len(t, forged, 1) + forgedWire, err := forged[0].MarshalBinary() + require.NoError(t, err) + require.Equal(t, wire, forgedWire) } func TestClassifyBufferedExpired(t *testing.T) { diff --git a/pkg/config/config.go b/pkg/config/config.go index 1e7416c90..d37fb3c0a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -235,6 +235,8 @@ type ValidatorConfig struct { TPUQUICBindAddr string `toml:"tpu_quic_bind_addr" mapstructure:"tpu_quic_bind_addr"` AdvertisedIP string `toml:"advertised_ip" mapstructure:"advertised_ip"` TPUSigverifyWorkers int `toml:"tpu_sigverify_workers" mapstructure:"tpu_sigverify_workers"` + BlockCompletionReserveMs int `toml:"block_completion_reserve_ms" mapstructure:"block_completion_reserve_ms"` + TPUMaxBufferedTransactions int `toml:"tpu_max_buffered_transactions" mapstructure:"tpu_max_buffered_transactions"` } // Config holds all configuration options for Mithril (Firedancer-style hierarchy) diff --git a/pkg/costmodel/entry_bytes.go b/pkg/costmodel/entry_bytes.go index ab91b8d5a..7cfec6481 100644 --- a/pkg/costmodel/entry_bytes.go +++ b/pkg/costmodel/entry_bytes.go @@ -28,8 +28,11 @@ func PackEntryBytesMax(slotMaxDataShreds, maxMicroblock uint64) uint64 { // DefaultPackEntryBytes is min(shred-safe, SIMD-0525) minus one ending tick. func DefaultPackEntryBytes() uint64 { - shredSafe := PackEntryBytesMax(DefaultMaxDataShredsPerSlot, MaxMicroblockBytes) - cap := uint64(DefaultMaxEntryBytesPerSlot) + return packEntryBytes(DefaultMaxDataShredsPerSlot, DefaultMaxEntryBytesPerSlot) +} + +func packEntryBytes(maxDataShreds, cap uint64) uint64 { + shredSafe := PackEntryBytesMax(maxDataShreds, MaxMicroblockBytes) if shredSafe > 0 && shredSafe < cap { cap = shredSafe } diff --git a/pkg/costmodel/limits.go b/pkg/costmodel/limits.go index ebf4869ad..4e3f726ff 100644 --- a/pkg/costmodel/limits.go +++ b/pkg/costmodel/limits.go @@ -1,7 +1,11 @@ package costmodel import ( + "fmt" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/safemath" + "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/gagliardetto/solana-go" ) @@ -75,11 +79,53 @@ func DefaultLimits() Limits { } } -// LimitsForFeatures returns the cost limits selected by the bank's feature set. +// LimitsForFeatures returns the legacy 400ms budgets. Live banks must use +// LimitsForSlot to apply slot-time reductions at the correct epoch boundary. func LimitsForFeatures(feats *features.Features) Limits { limits := DefaultLimits() if feats != nil && feats.IsActive(features.RaiseBlockLimitsTo100m) { limits.BlockCost = MaxBlockUnitsSIMD0286 + limits.WritableAccountCost = 40_000_000 } return limits } + +// LimitsForSlot mirrors Agave v4.3.0-rc.1 runtime/slot_params.rs. A slot-time +// gate takes effect in the epoch after activation; among effective gates the +// shortest duration wins, even if longer-duration gates activate later. +func LimitsForSlot(feats *features.Features, schedule *sealevel.SysvarEpochSchedule, slot uint64) (Limits, error) { + limits := DefaultLimits() + for _, transition := range []struct { + gate features.FeatureGate + account, block, data, shreds, entries uint64 + }{ + {features.ReduceSlotTimeTo350ms, 21_000_000, 52_500_000, 87_500_000, 28_672, 18_350_080}, + {features.ReduceSlotTimeTo300ms, 18_000_000, 45_000_000, 75_000_000, 24_576, 15_728_640}, + {features.ReduceSlotTimeTo250ms, 15_000_000, 37_500_000, 62_500_000, 20_480, 13_107_200}, + {features.ReduceSlotTimeTo200ms, 12_000_000, 30_000_000, 50_000_000, 16_384, 10_485_760}, + } { + if feats == nil { + break + } + activation, active := feats.ActivationSlot(transition.gate) + if !active { + continue + } + if schedule == nil || schedule.SlotsPerEpoch == 0 { + return Limits{}, fmt.Errorf("epoch schedule required for slot-time cost limits") + } + effective := schedule.FirstSlotInEpoch(safemath.SaturatingAddU64(schedule.GetEpoch(activation), 1)) + if effective > slot { + continue + } + limits.WritableAccountCost = transition.account + limits.BlockCost = transition.block + limits.AllocatedDataSizeDelta = transition.data + limits.MaxEntryBytes = packEntryBytes(transition.shreds, transition.entries) + } + if feats != nil && feats.IsActive(features.RaiseBlockLimitsTo100m) { + limits.BlockCost = limits.BlockCost * 100 / 60 + limits.WritableAccountCost = limits.WritableAccountCost * 100 / 60 + } + return limits, nil +} diff --git a/pkg/costmodel/slot_limits_test.go b/pkg/costmodel/slot_limits_test.go new file mode 100644 index 000000000..0d521e744 --- /dev/null +++ b/pkg/costmodel/slot_limits_test.go @@ -0,0 +1,88 @@ +package costmodel + +import ( + "testing" + + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/stretchr/testify/require" +) + +func TestSlotLimitsMatchAgaveTable(t *testing.T) { + schedule := &sealevel.SysvarEpochSchedule{SlotsPerEpoch: 100} + for _, tc := range []struct { + name string + gate features.FeatureGate + account, block, data, shreds, entries uint64 + }{ + {"400ms", features.FeatureGate{}, 24_000_000, 60_000_000, 100_000_000, 32768, 20 * 1024 * 1024}, + {"350ms", features.ReduceSlotTimeTo350ms, 21_000_000, 52_500_000, 87_500_000, 28672, 18_350_080}, + {"300ms", features.ReduceSlotTimeTo300ms, 18_000_000, 45_000_000, 75_000_000, 24576, 15_728_640}, + {"250ms", features.ReduceSlotTimeTo250ms, 15_000_000, 37_500_000, 62_500_000, 20480, 13_107_200}, + {"200ms", features.ReduceSlotTimeTo200ms, 12_000_000, 30_000_000, 50_000_000, 16384, 10_485_760}, + } { + t.Run(tc.name, func(t *testing.T) { + f := features.NewFeaturesDefault() + if tc.name != "400ms" { + f.EnableFeature(tc.gate, 50) + } + before, err := LimitsForSlot(f, schedule, 99) + require.NoError(t, err) + require.Equal(t, DefaultLimits(), before) + for _, raise := range []bool{false, true} { + if raise { + f.EnableFeature(features.RaiseBlockLimitsTo100m, 0) + } + got, err := LimitsForSlot(f, schedule, 100) + require.NoError(t, err) + account, block := tc.account, tc.block + if raise { + account = account * 100 / 60 + block = block * 100 / 60 + } + require.Equal(t, account, got.WritableAccountCost) + require.Equal(t, block, got.BlockCost) + require.Equal(t, tc.data, got.AllocatedDataSizeDelta) + require.Equal(t, tc.entries-EntryHeaderBytes, got.MaxEntryBytes) + require.LessOrEqual(t, got.MaxEntryBytes, PackEntryBytesMax(tc.shreds, MaxMicroblockBytes)) + require.Equal(t, uint64(DefaultTargetBatchBytes), got.MaxBatchBytes) + } + }) + } +} + +func TestSlotLimitsDoNotLengthenSlotsForLaterGates(t *testing.T) { + f := features.NewFeaturesDefault() + f.EnableFeature(features.ReduceSlotTimeTo200ms, 50) + f.EnableFeature(features.ReduceSlotTimeTo350ms, 150) + schedule := &sealevel.SysvarEpochSchedule{SlotsPerEpoch: 100} + for _, slot := range []uint64{100, 199, 200, 400} { + limits, err := LimitsForSlot(f, schedule, slot) + require.NoError(t, err) + require.Equal(t, uint64(30_000_000), limits.BlockCost) + } +} + +func TestSlotLimitsActivationDuringWarmup(t *testing.T) { + f := features.NewFeaturesDefault() + f.EnableFeature(features.ReduceSlotTimeTo200ms, 40) + // Epoch 0: [0,32), epoch 1: [32,96), normal epoch 2: [96,224). + schedule := &sealevel.SysvarEpochSchedule{Warmup: true, SlotsPerEpoch: 128, FirstNormalEpoch: 2, FirstNormalSlot: 96} + before, err := LimitsForSlot(f, schedule, 95) + require.NoError(t, err) + require.Equal(t, uint64(60_000_000), before.BlockCost) + after, err := LimitsForSlot(f, schedule, 96) + require.NoError(t, err) + require.Equal(t, uint64(30_000_000), after.BlockCost) +} + +func TestSlotLimitsRequireScheduleForActiveReductions(t *testing.T) { + f := features.NewFeaturesDefault() + _, err := LimitsForSlot(f, nil, 10) + require.NoError(t, err) + f.EnableFeature(features.ReduceSlotTimeTo200ms, 0) + _, err = LimitsForSlot(f, nil, 10) + require.Error(t, err) + _, err = LimitsForSlot(f, &sealevel.SysvarEpochSchedule{}, 10) + require.Error(t, err) +} diff --git a/pkg/costmodel/transaction_cost.go b/pkg/costmodel/transaction_cost.go index 1dd4f0c71..3a804b037 100644 --- a/pkg/costmodel/transaction_cost.go +++ b/pkg/costmodel/transaction_cost.go @@ -55,6 +55,13 @@ func EstimateTransactionCost(tx *solana.Transaction, feats *features.Features) ( }, nil } + return EstimatePreparedTransactionCost(tx, instrs, limits, feats), nil +} + +// EstimatePreparedTransactionCost reuses successfully parsed instructions and +// compute limits from the same immutable transaction and feature snapshot. +func EstimatePreparedTransactionCost(tx *solana.Transaction, instrs []sealevel.Instruction, limits *sealevel.ComputeBudgetLimits, feats *features.Features) TransactionCost { + writable := writableAccounts(tx) loadedDataCost := loadedAccountsDataSizeCost(limits.LoadedAccountBytes) // Banking-stage admission must reserve at least one page for the fee // payer, including V1 transactions whose inline loaded-data limit is zero. @@ -62,13 +69,13 @@ func EstimateTransactionCost(tx *solana.Transaction, feats *features.Features) ( loadedDataCost = max(loadedDataCost, uint64(HeapCost)) return TransactionCost{ SignatureCost: signatureCost(tx, instrs, feats), - WriteLockCost: writeLockCost(countWriteLocks(tx)), + WriteLockCost: writeLockCost(uint64(len(writable))), DataBytesCost: instructionDataCost(tx), ProgramsExecutionCost: uint64(limits.ComputeUnitLimit), LoadedAccountsDataSizeCost: loadedDataCost, AllocatedAccountsDataSize: estimateAllocDelta(instrs, feats), - WritableAccounts: writableAccounts(tx), - }, nil + WritableAccounts: writable, + } } func signatureCost(tx *solana.Transaction, instrs []sealevel.Instruction, feats *features.Features) uint64 { @@ -177,7 +184,7 @@ func replayInstrsAndAcctMetas(tx *solana.Transaction, feats *features.Features) } upgradeableLoaderPresent := false for _, key := range tx.Message.AccountKeys { - if key.String() == "BPFLoaderUpgradeab1e11111111111111111111111" { + if key == addresses.BpfLoaderUpgradeableAddr { upgradeableLoaderPresent = true break } diff --git a/pkg/merkletree/merkletree.go b/pkg/merkletree/merkletree.go index 988f2633c..0de1d98f3 100644 --- a/pkg/merkletree/merkletree.go +++ b/pkg/merkletree/merkletree.go @@ -46,8 +46,29 @@ func (n *Nodes) GetRoot() (out *[32]byte) { return &n.Nodes[len(n.Nodes)-1] } -// TODO provide a method for memory-efficient Merkle construction when only the root is requested. -// Can be implemented using recursion root level downwards +// HashRoot computes the same root as HashNodes without retaining proof nodes. +// Empty input returns zero. Each level overwrites the preceding level, so only +// one hash per leaf is allocated. Leaves are never modified. +func HashRoot(leaves [][]byte) (root [32]byte) { + if len(leaves) == 0 { + return root + } + if len(leaves) == 1 { + return HashLeaf(leaves[0]) + } + nodes := make([][32]byte, len(leaves)) + for i, leaf := range leaves { + nodes[i] = HashLeaf(leaf) + } + for len(nodes) > 1 { + for i := 0; i < len(nodes); i += 2 { + right := min(i+1, len(nodes)-1) + nodes[i/2] = HashIntermediate(&nodes[i], &nodes[right]) + } + nodes = nodes[:(len(nodes)+1)/2] + } + return nodes[0] +} // HashNodes constructs proof data from a set of leaves. // @@ -97,6 +118,14 @@ func HashNodes(leaves [][]byte) (out Nodes) { // HashLeaf returns the hash of a leaf node. func HashLeaf(data []byte) (out [32]byte) { + if len(data) == 64 { + // Transaction signatures are fixed-width leaves. A single buffer avoids + // incremental hash writes while retaining the leaf domain separator. + var input [65]byte + input[0] = TypeLeaf + copy(input[1:], data) + return sha256.Sum256(input[:]) + } h := sha256.New() h.Write([]byte{TypeLeaf}) h.Write(data) @@ -106,12 +135,11 @@ func HashLeaf(data []byte) (out [32]byte) { // HashIntermediate returns the hash of an intermediate node. func HashIntermediate(left *[32]byte, right *[32]byte) (out [32]byte) { - h := sha256.New() - h.Write([]byte{TypeIntermediate}) - h.Write(left[:]) - h.Write(right[:]) - h.Sum(out[:0]) - return + var input [65]byte + input[0] = TypeIntermediate + copy(input[1:33], left[:]) + copy(input[33:], right[:]) + return sha256.Sum256(input[:]) } // nextLevelLen returns the amount of nodes in the layer above the current one, diff --git a/pkg/merkletree/root_test.go b/pkg/merkletree/root_test.go new file mode 100644 index 000000000..1437c3833 --- /dev/null +++ b/pkg/merkletree/root_test.go @@ -0,0 +1,60 @@ +package merkletree + +import ( + "bytes" + "crypto/sha256" + "fmt" + "testing" +) + +// Independent construction: retain separate levels and use one-shot SHA256 +// over explicit domain-prefixed bytes, without the production hash helpers. +func referenceRoot(leaves [][]byte) [32]byte { + if len(leaves) == 0 { + return [32]byte{} + } + level := make([][32]byte, len(leaves)) + for i, leaf := range leaves { + level[i] = sha256.Sum256(append([]byte{0}, leaf...)) + } + for len(level) > 1 { + next := make([][32]byte, (len(level)+1)/2) + for i := range next { + left, right := level[2*i], level[min(2*i+1, len(level)-1)] + data := append([]byte{1}, left[:]...) + data = append(data, right[:]...) + next[i] = sha256.Sum256(data) + } + level = next + } + return level[0] +} + +func TestHashRootMatchesCanonicalTree(t *testing.T) { + counts := []int{0, 1, 2, 3, 7, 8, 9, 31, 32, 33, 63, 64, 65, 127, 128, 129, 255, 256, 257, 311, 511, 512, 513, 1023, 1024, 1025} + for _, size := range []int{0, 31, 32, 55, 56, 63, 64, 65, 1232} { + for _, count := range counts { + t.Run(fmt.Sprintf("%dx%d", count, size), func(t *testing.T) { + leaves := make([][]byte, count) + for i := range leaves { + leaves[i] = make([]byte, size) + for j := range leaves[i] { + leaves[i][j] = byte(i*71 + i/256 + j*17) + } + } + before := bytes.Join(leaves, nil) + want := referenceRoot(leaves) + if got := HashRoot(leaves); got != want { + t.Fatalf("root %x, want %x", got, want) + } + nodes := HashNodes(leaves) + if got := nodes.GetRoot(); got != nil && *got != want { + t.Fatalf("proof root %x, want %x", *got, want) + } + if !bytes.Equal(before, bytes.Join(leaves, nil)) { + t.Fatal("hashing modified input leaves") + } + }) + } + } +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index fee3c92fb..3f87fa287 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -15,8 +15,18 @@ func (t *Timing) AddTiming(d time.Duration) { atomic.AddUint64(&t.SumNanoseconds, uint64(d.Nanoseconds())) } +// StartTiming avoids reading the clock when a caller does not record timings. +func StartTiming(enabled bool) time.Time { + if enabled { + return time.Now() + } + return time.Time{} +} + func (t *Timing) AddTimingSince(start time.Time) { - t.AddTiming(time.Since(start)) + if !start.IsZero() { + t.AddTiming(time.Since(start)) + } } // AccountLoader is the per-slot decomposition of LoadBlockAccounts. Counters diff --git a/pkg/replay/chain_state.go b/pkg/replay/chain_state.go index 649146c93..70c404b22 100644 --- a/pkg/replay/chain_state.go +++ b/pkg/replay/chain_state.go @@ -266,3 +266,14 @@ func ChainTipFeatureActive(gate features.FeatureGate) bool { defer chainTipMu.RUnlock() return chainTipFeatures != nil && chainTipFeatures.IsActive(gate) } + +// ChainTipFeatures returns an independent feature snapshot for queued transaction +// preparation. Bank admission checks compatibility again before reuse. +func ChainTipFeatures() *features.Features { + chainTipMu.RLock() + defer chainTipMu.RUnlock() + if chainTipFeatures == nil { + return nil + } + return chainTipFeatures.Clone() +} diff --git a/pkg/replay/transaction_preparation.go b/pkg/replay/transaction_preparation.go new file mode 100644 index 000000000..303dcc3e3 --- /dev/null +++ b/pkg/replay/transaction_preparation.go @@ -0,0 +1,133 @@ +package replay + +import ( + "crypto/sha256" + "encoding/binary" + "sort" + + "github.com/Overclock-Validator/mithril/pkg/costmodel" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/Overclock-Validator/mithril/pkg/fees" + "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/Overclock-Validator/mithril/pkg/txverify" + "github.com/gagliardetto/solana-go" +) + +// TransactionPreparer binds static transaction preparation to an immutable bank +// feature snapshot. Its source must remain immutable for the bank's lifetime. +// Prepared messages must also remain immutable, including their backing bytes. +// Account contents, transaction age and duplicate status are never cached here. +type TransactionPreparer struct { + source *features.Features + feats *features.Features + key [32]byte +} + +// PreparedTransaction contains only message/feature-derived data. Private fields +// prevent callers from substituting an unchecked hash, cost or instruction list. +type PreparedTransaction struct { + tx *solana.Transaction + key [32]byte + hash [32]byte + cost costmodel.TransactionCost + instrs []sealevel.Instruction + instructionAccts [][]sealevel.InstructionAccount + accountMetas []*solana.AccountMeta + limits *sealevel.ComputeBudgetLimits +} + +func NewTransactionPreparer(f *features.Features) *TransactionPreparer { + if f == nil { + return nil + } + clone := f.Clone() + gates := make([]features.FeatureGate, 0, len(*clone)) + for gate := range *clone { + gates = append(gates, gate) + } + sort.Slice(gates, func(i, j int) bool { + if gates[i].Address != gates[j].Address { + return string(gates[i].Address[:]) < string(gates[j].Address[:]) + } + return gates[i].Name < gates[j].Name + }) + h := sha256.New() + var value [8]byte + for _, gate := range gates { + info := (*clone)[gate] + h.Write(gate.Address[:]) + binary.LittleEndian.PutUint64(value[:], uint64(len(gate.Name))) + h.Write(value[:]) + h.Write([]byte(gate.Name)) + if info.Enabled { + h.Write([]byte{1}) + } else { + h.Write([]byte{0}) + } + binary.LittleEndian.PutUint64(value[:], info.ActivationSlot) + h.Write(value[:]) + } + p := &TransactionPreparer{source: f, feats: clone} + copy(p.key[:], h.Sum(nil)) + return p +} + +// Prepare returns nil on a static validation failure. Callers retain their normal +// processing path in that case, preserving its error classification and ordering. +func (p *TransactionPreparer) Prepare(tx *solana.Transaction) *PreparedTransaction { + if p == nil || tx == nil { + return nil + } + if tx.Message.GetVersion() == solana.MessageVersionV1 && !p.feats.IsActive(features.EnableTxV1) { + return nil + } + if txverify.SanitizeTransaction(tx) != nil { + return nil + } + if p.feats.IsActive(features.StaticInstructionLimit) && len(tx.Message.Instructions) > maxInstrTraceCapacity { + return nil + } + instrs, instructionAccts, metas, err := instrsAndAcctMetasFromTx(tx, p.feats) + if err != nil { + return nil + } + limits, err := sealevel.ComputeBudgetLimitsForTransaction(tx, instrs, p.feats) + if err != nil { + return nil + } + hash, err := TransactionMessageHash(tx) + if err != nil { + return nil + } + return &PreparedTransaction{tx: tx, key: p.key, hash: hash, + cost: costmodel.EstimatePreparedTransactionCost(tx, instrs, limits, p.feats), + instrs: instrs, instructionAccts: instructionAccts, accountMetas: metas, limits: limits} +} + +func (p *TransactionPreparer) Matches(prepared *PreparedTransaction, tx *solana.Transaction, f *features.Features) bool { + return p != nil && p.source == f && prepared != nil && prepared.tx == tx && p.key == prepared.key +} + +func (p *PreparedTransaction) MessageHash() [32]byte { return p.hash } + +// Cost returns the estimate; its writable-account slice is read-only. +func (p *PreparedTransaction) Cost() costmodel.TransactionCost { return p.cost } + +// LoadAndExecute reuses preparation only for the same immutable message and a +// matching feature snapshot. All bank-dependent checks still run on every call. +func (p *TransactionPreparer) LoadAndExecute(input LoadAndExecuteTransactionInput, prepared *PreparedTransaction) LoadAndExecuteTransactionOutput { + if input.SlotCtx == nil || p == nil || p.source != input.SlotCtx.Features || !p.Matches(prepared, input.Transaction, input.SlotCtx.Features) { + return LoadAndExecuteTransaction(input) + } + return loadAndExecuteTransaction(input, prepared) +} + +// PayerCanFund keeps strict leader admission, using current payer state while +// sharing the already-validated instructions and compute limits. +func (p *TransactionPreparer) PayerCanFund(slotCtx *sealevel.SlotCtx, tx *solana.Transaction, prepared *PreparedTransaction) error { + if slotCtx == nil || !p.Matches(prepared, tx, slotCtx.Features) { + return fees.PayerCanFund(slotCtx, tx) + } + _, err := fees.ValidateTransactionFeePayer(slotCtx, tx, prepared.instrs, prepared.limits) + return err +} diff --git a/pkg/replay/transaction_processing_pure.go b/pkg/replay/transaction_processing_pure.go index 061703db1..434346460 100644 --- a/pkg/replay/transaction_processing_pure.go +++ b/pkg/replay/transaction_processing_pure.go @@ -3,7 +3,6 @@ package replay import ( "errors" "math" - "time" "github.com/Overclock-Validator/mithril/pkg/accounts" "github.com/Overclock-Validator/mithril/pkg/arena" @@ -39,6 +38,10 @@ type LoadAndExecuteTransactionInput struct { // banks also skip writable-account result materialization; RPC simulation // leaves this false to retain the rich result. LeanResult bool + // SkipTimingMetrics omits the detailed transaction and instruction-dispatch + // timings for leader execution. Replay and simulation retain their default + // instrumentation; program-specific instrumentation is independent. + SkipTimingMetrics bool // CapturePreBalances retains pre-fee balances in lean mode. Rich mode // always captures them for RPC compatibility. CapturePreBalances bool @@ -124,6 +127,10 @@ func feeOnlyRollbackAccountsDataSize(slotCtx *sealevel.SlotCtx, tx *solana.Trans } func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExecuteTransactionOutput { + return loadAndExecuteTransaction(input, nil) +} + +func loadAndExecuteTransaction(input LoadAndExecuteTransactionInput, prepared *PreparedTransaction) LoadAndExecuteTransactionOutput { tx := input.Transaction slotCtx := input.SlotCtx @@ -131,64 +138,76 @@ func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExec input.Arena.Reset() } - if tx == nil || slotCtx == nil || slotCtx.Features == nil { - return sanitizeFailureOutput() - } + var instrs []sealevel.Instruction + var instructionAcctsPerInstr [][]sealevel.InstructionAccount + var txAcctMetas []*solana.AccountMeta + var computeBudgetLimits *sealevel.ComputeBudgetLimits + var err error + start := metrics.StartTiming(false) + if prepared != nil { + instrs, instructionAcctsPerInstr, txAcctMetas = prepared.instrs, prepared.instructionAccts, prepared.accountMetas + computeBudgetLimits = prepared.limits + } else { + if tx == nil || slotCtx == nil || slotCtx.Features == nil { + return sanitizeFailureOutput() + } - // Match Agave's Bank verification order: once a transaction has decoded as - // v1, the feature gate is checked before structural sanitization. - if tx.Message.GetVersion() == solana.MessageVersionV1 && !slotCtx.Features.IsActive(features.EnableTxV1) { - return LoadAndExecuteTransactionOutput{ - ProcessingResult: TransactionProcessingResult{ - TransactionError: &TransactionError{ - ErrorType: TransactionErrorUnsupportedVersion, - InstructionError: TxErrUnsupportedVersion, + // Match Agave's Bank verification order: once a transaction has decoded as + // v1, the feature gate is checked before structural sanitization. + if tx.Message.GetVersion() == solana.MessageVersionV1 && !slotCtx.Features.IsActive(features.EnableTxV1) { + return LoadAndExecuteTransactionOutput{ + ProcessingResult: TransactionProcessingResult{ + TransactionError: &TransactionError{ + ErrorType: TransactionErrorUnsupportedVersion, + InstructionError: TxErrUnsupportedVersion, + }, }, - }, + } + } + // Reject malformed transactions before account-indexed code can observe + // them. The helper also handles Mithril's already-resolved v0 messages. + if err := txverify.SanitizeTransaction(tx); err != nil { + return sanitizeFailureOutput() + } + // Mirror block-replay's StaticInstructionLimit cap so pre-activation + // clusters fail mid-execution like Agave instead of SanitizeFailure. + if slotCtx.Features.IsActive(features.StaticInstructionLimit) && + len(tx.Message.Instructions) > maxInstrTraceCapacity { + return sanitizeFailureOutput() } - } - // Reject malformed transactions before account-indexed code can observe - // them. The helper also handles Mithril's already-resolved v0 messages. - if err := txverify.SanitizeTransaction(tx); err != nil { - return sanitizeFailureOutput() - } - // Mirror block-replay's StaticInstructionLimit cap so pre-activation - // clusters fail mid-execution like Agave instead of SanitizeFailure. - if slotCtx.Features.IsActive(features.StaticInstructionLimit) && - len(tx.Message.Instructions) > maxInstrTraceCapacity { - return sanitizeFailureOutput() - } - // Parse instructions and account metas - start := time.Now() - instrs, instructionAcctsPerInstr, txAcctMetas, err := instrsAndAcctMetasFromTx(tx, slotCtx.Features) - if err != nil { - return LoadAndExecuteTransactionOutput{ - ProcessingResult: TransactionProcessingResult{ - TransactionError: &TransactionError{ - ErrorType: TransactionErrorSanitizeFailure, - InstructionError: err, + // Parse instructions and account metas + start = metrics.StartTiming(!input.SkipTimingMetrics) + instrs, instructionAcctsPerInstr, txAcctMetas, err = instrsAndAcctMetasFromTx(tx, slotCtx.Features) + if err != nil { + return LoadAndExecuteTransactionOutput{ + ProcessingResult: TransactionProcessingResult{ + TransactionError: &TransactionError{ + ErrorType: TransactionErrorSanitizeFailure, + InstructionError: err, + }, }, - }, + } } - } - metrics.GlobalBlockReplay.InstructionsAndAccountMetasFromTx.AddTimingSince(start) - - // Compute budget limits - start = time.Now() - computeBudgetLimits, err := sealevel.ComputeBudgetLimitsForTransaction(tx, instrs, slotCtx.Features) - if err != nil { - return LoadAndExecuteTransactionOutput{ - ProcessingResult: TransactionProcessingResult{ - TransactionError: &TransactionError{ - ErrorType: TransactionErrorInstructionError, - InstructionError: err, + metrics.GlobalBlockReplay.InstructionsAndAccountMetasFromTx.AddTimingSince(start) + + // Compute budget limits + start = metrics.StartTiming(!input.SkipTimingMetrics) + computeBudgetLimits, err = sealevel.ComputeBudgetLimitsForTransaction(tx, instrs, slotCtx.Features) + if err != nil { + return LoadAndExecuteTransactionOutput{ + ProcessingResult: TransactionProcessingResult{ + TransactionError: &TransactionError{ + ErrorType: TransactionErrorInstructionError, + InstructionError: err, + }, }, - }, - Instrs: instrs, + Instrs: instrs, + } } + metrics.GlobalBlockReplay.ComputeBudgetExecutionInstructions.AddTimingSince(start) + } - metrics.GlobalBlockReplay.ComputeBudgetExecutionInstructions.AddTimingSince(start) // Validate transaction age if !sealevel.IsTransactionAgeValid(tx, instrs, slotCtx) { @@ -236,7 +255,7 @@ func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExec } // Load and validate accounts - start = time.Now() + start = metrics.StartTiming(!input.SkipTimingMetrics) instructionsSysvarIdx := instructionsSysvarAccountIndex(tx) var instrsAcct *accounts.Account if instructionsSysvarIdx >= 0 { @@ -303,6 +322,7 @@ func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExec execCtx.TransactionContext.Signature = tx.Signatures[0] execCtx.TransactionContext.BorrowedAccountArena = input.Arena execCtx.IsSimulation = input.IsSimulation + execCtx.SkipTimingMetrics = input.SkipTimingMetrics execCtx.RecordInnerInstructions = input.RecordInnerInstructions // Capture pre-balance lamports (before fee deduction) @@ -332,7 +352,7 @@ func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExec // Calculate and deduct fees. RentForSlot supplies the exemption // minimum so a rent-exempt payer is rejected before instructions run. - start = time.Now() + start = metrics.StartTiming(!input.SkipTimingMetrics) txFeeInfo, _, err := fees.CalculateAndDeductTxFees(tx, input.TxMeta, instrs, &execCtx.TransactionContext.Accounts, computeBudgetLimits, slotCtx.Features, fees.RentForSlot(slotCtx), input.IsSimulation) if err != nil { errType, accountIndex := feePayerTransactionError(err) @@ -355,7 +375,7 @@ func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExec metrics.GlobalBlockReplay.CalcAndDeductFees.AddTimingSince(start) // Read rent sysvar - start = time.Now() + start = metrics.StartTiming(!input.SkipTimingMetrics) rentSysvar, err := sealevel.ReadRentSysvar(execCtx) if err != nil { // Rent sysvar unreadable; return cleanly so the RPC worker @@ -374,18 +394,18 @@ func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExec metrics.GlobalBlockReplay.ReadRentSysvar.AddTimingSince(start) // Set rent-exempt rent epoch max and compute pre-tx rent states - start = time.Now() + start = metrics.StartTiming(!input.SkipTimingMetrics) rent.MaybeSetRentExemptRentEpochMax(slotCtx, &rentSysvar, &execCtx.Features, &execCtx.TransactionContext.Accounts) preTxRentStates := rent.NewRentStateInfo(&rentSysvar, execCtx.TransactionContext, &execCtx.Features) metrics.GlobalBlockReplay.PreTxRentStates.AddTimingSince(start) // Execute all instructions var instrErr error - start = time.Now() + start = metrics.StartTiming(!input.SkipTimingMetrics) for instrIdx, instr := range tx.Message.Instructions { execCtx.SetCurrentTopLevelInstr(uint8(instrIdx)) if instructionsSysvarIdx >= 0 { - ixStart := time.Now() + ixStart := metrics.StartTiming(!input.SkipTimingMetrics) err = fixupInstructionsSysvarAcct(execCtx, instructionsSysvarIdx, uint16(instrIdx)) if err != nil { instrErr = err @@ -421,7 +441,7 @@ func LoadAndExecuteTransaction(input LoadAndExecuteTransactionInput) LoadAndExec metrics.GlobalBlockReplay.IxLoop.AddTimingSince(start) // Check rent state transitions - start = time.Now() + start = metrics.StartTiming(!input.SkipTimingMetrics) postTxRentStates := rent.NewRentStateInfo(&rentSysvar, execCtx.TransactionContext, &execCtx.Features) rentStateErr := rent.VerifyRentStateChanges(preTxRentStates, postTxRentStates, execCtx.TransactionContext) metrics.GlobalBlockReplay.PostTxRentStates.AddTimingSince(start) diff --git a/pkg/sealevel/execution_ctx.go b/pkg/sealevel/execution_ctx.go index 348b4647c..ba54cc267 100644 --- a/pkg/sealevel/execution_ctx.go +++ b/pkg/sealevel/execution_ctx.go @@ -5,7 +5,6 @@ import ( "fmt" "sync" "sync/atomic" - "time" "github.com/Overclock-Validator/mithril/pkg/accounts" "github.com/Overclock-Validator/mithril/pkg/accountsdb" @@ -19,6 +18,9 @@ import ( ) type ExecutionCtx struct { + // SkipTimingMetrics disables instruction-dispatch timing collection for + // leader execution; it never changes instruction validation or CU charging. + SkipTimingMetrics bool Log Logger Accounts accounts.Accounts TransactionContext *TransactionCtx @@ -237,14 +239,14 @@ func (execCtx *ExecutionCtx) PrepareInstruction(ix Instruction, signers []solana } func (execCtx *ExecutionCtx) ProcessInstruction(instrData []byte, instructionAccts []InstructionAccount, programIndices []uint64) error { - start := time.Now() + start := metrics.StartTiming(!execCtx.SkipTimingMetrics) nextInstrCtx, err := execCtx.TransactionContext.NextInstructionCtx() if err != nil { return err } metrics.GlobalBlockReplay.GetNextIxCtx.AddTimingSince(start) - start = time.Now() + start = metrics.StartTiming(!execCtx.SkipTimingMetrics) nextInstrCtx.Configure(programIndices, instructionAccts, instrData) metrics.GlobalBlockReplay.NextIxCtxConfigure.AddTimingSince(start) @@ -267,7 +269,7 @@ func (execCtx *ExecutionCtx) ProcessInstruction(instrData []byte, instructionAcc }) } - start = time.Now() + start = metrics.StartTiming(!execCtx.SkipTimingMetrics) err = execCtx.Push() if err != nil { return err @@ -283,7 +285,7 @@ func (execCtx *ExecutionCtx) ProcessInstruction(instrData []byte, instructionAcc err1 := execCtx.ExecuteInstruction() - start = time.Now() + start = metrics.StartTiming(!execCtx.SkipTimingMetrics) err2 := execCtx.Pop() metrics.GlobalBlockReplay.IxPop.AddTimingSince(start) @@ -304,7 +306,7 @@ func (execCtx *ExecutionCtx) AddModifiedVoteState(pubkey solana.PublicKey, state } func (execCtx *ExecutionCtx) ExecuteInstruction() error { - start := time.Now() + start := metrics.StartTiming(!execCtx.SkipTimingMetrics) txCtx := execCtx.TransactionContext instrCtx, err := txCtx.CurrentInstructionCtx() @@ -334,7 +336,7 @@ func (execCtx *ExecutionCtx) ExecuteInstruction() error { } metrics.GlobalBlockReplay.ExecIxResolveNativeProgram.AddTimingSince(start) - start = time.Now() + start = metrics.StartTiming(!execCtx.SkipTimingMetrics) err = nativeProgramFn(execCtx) switch nativeProgramStr { case a.SystemProgramAddrStr: diff --git a/pkg/tpu/txfixture/readonly_pair.go b/pkg/tpu/txfixture/readonly_pair.go new file mode 100644 index 000000000..c1de0a074 --- /dev/null +++ b/pkg/tpu/txfixture/readonly_pair.go @@ -0,0 +1,42 @@ +package txfixture + +import ( + "crypto/ed25519" + "fmt" + + "github.com/gagliardetto/solana-go" +) + +const ReadonlyPairPoolSize = 128 +const ReadonlyPairCapacity = ReadonlyPairPoolSize * (ReadonlyPairPoolSize - 1) + +// ReadonlyPairWire builds the 198-byte, single-signature, zero-instruction +// workload used for leader packing tests. Varying ordered pairs of existing +// readonly accounts gives distinct messages without adding instructions or +// forcing a lookup of a new nonexistent account for every transaction. +// Reusing an ordinal requires a different payer or recent blockhash. +func ReadonlyPairWire(key ed25519.PrivateKey, hash solana.Hash, pool []solana.PublicKey, ordinal int) ([]byte, error) { + if len(key) != ed25519.PrivateKeySize || len(pool) != ReadonlyPairPoolSize || ordinal < 0 || ordinal >= ReadonlyPairCapacity { + return nil, fmt.Errorf("invalid readonly-pair fixture key, pool or ordinal") + } + n := ordinal * 7919 % ReadonlyPairCapacity + a, b := n/(ReadonlyPairPoolSize-1), n%(ReadonlyPairPoolSize-1) + if b >= a { + b++ + } + payer := solana.PublicKeyFromBytes(key.Public().(ed25519.PublicKey)) + if payer == pool[a] || payer == pool[b] || pool[a] == pool[b] { + return nil, fmt.Errorf("readonly-pair accounts must be distinct") + } + message := make([]byte, 0, 133) + message = append(message, 1, 0, 2, 3) + message = append(message, payer[:]...) + message = append(message, pool[a][:]...) + message = append(message, pool[b][:]...) + message = append(message, hash[:]...) + message = append(message, 0) + wire := make([]byte, 0, 198) + wire = append(wire, 1) + wire = append(wire, ed25519.Sign(key, message)...) + return append(wire, message...), nil +} diff --git a/pkg/tpu/txfixture/readonly_pair_test.go b/pkg/tpu/txfixture/readonly_pair_test.go new file mode 100644 index 000000000..70efa16dd --- /dev/null +++ b/pkg/tpu/txfixture/readonly_pair_test.go @@ -0,0 +1,68 @@ +package txfixture + +import ( + "crypto/ed25519" + "crypto/sha256" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// Reproduce the full preloaded four-slot experiment without RPC, funds or sends. +func TestReadonlyPair200KDistinctMessages(t *testing.T) { + keys := make([]ed25519.PrivateKey, 8) + for i := range keys { + seed := sha256.Sum256([]byte{byte(i), 73}) + keys[i] = ed25519.NewKeyFromSeed(seed[:]) + } + pool := make([]solana.PublicKey, ReadonlyPairPoolSize) + for i := range pool { + pool[i] = solana.PublicKey{byte(i + 1), 77} + } + seen := make(map[[32]byte]struct{}, 200000) + for i := 0; i < 200000; i++ { + hash, index := solana.Hash{1}, i + if i >= 120000 { + hash, index = solana.Hash{2}, i-120000 + } + wire, err := ReadonlyPairWire(keys[index%8], hash, pool, index/8) + require.NoError(t, err) + require.Len(t, wire, 198) + h := sha256.Sum256(wire[65:]) + if _, ok := seen[h]; ok { + t.Fatalf("duplicate message %d", i) + } + seen[h] = struct{}{} + if i%ReadonlyPairCapacity == 0 || i == 119999 || i == 120000 || i == 199999 { + tx, err := solana.TransactionFromBytes(wire) + require.NoError(t, err) + require.Len(t, tx.Signatures, 1) + require.Empty(t, tx.Message.Instructions) + require.Equal(t, hash, tx.Message.RecentBlockhash) + require.True(t, ed25519.Verify(ed25519.PublicKey(tx.Message.AccountKeys[0][:]), wire[65:], wire[1:65])) + } + } +} + +func TestReadonlyPairRejectsInvalidInputs(t *testing.T) { + key := ed25519.PrivateKey(PayerPrivateKey()) + pool := make([]solana.PublicKey, ReadonlyPairPoolSize) + for i := range pool { + pool[i] = solana.PublicKey{byte(i + 1), 77} + } + for _, ordinal := range []int{-1, ReadonlyPairCapacity} { + _, err := ReadonlyPairWire(key, TestBlockhash(), pool, ordinal) + require.Error(t, err) + } + _, err := ReadonlyPairWire(nil, TestBlockhash(), pool, 0) + require.Error(t, err) + _, err = ReadonlyPairWire(key, TestBlockhash(), nil, 0) + require.Error(t, err) + pool[0] = solana.PublicKeyFromBytes(key.Public().(ed25519.PublicKey)) + _, err = ReadonlyPairWire(key, TestBlockhash(), pool, 0) + require.Error(t, err) + pool[0], pool[1] = solana.PublicKey{9}, solana.PublicKey{9} + _, err = ReadonlyPairWire(key, TestBlockhash(), pool, 0) + require.Error(t, err) +} diff --git a/pkg/turbine/entry_hash.go b/pkg/turbine/entry_hash.go index 72af6bdb8..35ee7561c 100644 --- a/pkg/turbine/entry_hash.go +++ b/pkg/turbine/entry_hash.go @@ -90,14 +90,7 @@ func hashTransactions(txns []solana.Transaction) solana.Hash { } func hashSignatures(signatures [][]byte) solana.Hash { - if len(signatures) == 0 { - return solana.Hash{} - } - nodes := merkletree.HashNodes(signatures) - if root := nodes.GetRoot(); root != nil { - return solana.Hash(*root) - } - return solana.Hash{} + return solana.Hash(merkletree.HashRoot(signatures)) } func sha256Hash(data []byte) solana.Hash { diff --git a/pkg/turbine/entry_hash_bench_test.go b/pkg/turbine/entry_hash_bench_test.go new file mode 100644 index 000000000..e8d7d1038 --- /dev/null +++ b/pkg/turbine/entry_hash_bench_test.go @@ -0,0 +1,24 @@ +package turbine + +import ( + "encoding/binary" + "fmt" + "testing" +) + +func BenchmarkEntrySignatureRoot(b *testing.B) { + for _, count := range []int{1, 64, 311, 512, 1024} { + b.Run(fmt.Sprint(count), func(b *testing.B) { + sigs := make([][]byte, count) + for i := range sigs { + sigs[i] = make([]byte, 64) + binary.LittleEndian.PutUint64(sigs[i], uint64(i)) + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = hashSignatures(sigs) + } + }) + } +} From 828e001d08d9d80ca3713d96a43ea8d7afe2dee3 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:16:07 -0500 Subject: [PATCH 2/9] Document current-base leader benchmarks and finalized load results --- docs/leader_block_packing.md | 94 ++++ .../leader-block-packing/2026-09-13/README.md | 129 +++++ .../2026-09-13/baseline-capacity.txt | 1 + .../best-block/all-finalized-costs.json | 56 ++ .../best-block/largest-block-metrics.json | 16 + .../2026-09-13/best-block/metadata.json | 17 + .../2026-09-13/best-block/send.json | 12 + .../2026-09-13/best-block/validation.json | 99 ++++ .../2026-09-13/binary-sha256.txt | 3 + .../2026-09-13/evidence-manifest.json | 52 ++ .../all-finalized-costs.json | 35 ++ .../largest-block-metrics.json | 16 + .../four-block-prefill/metadata.json | 17 + .../2026-09-13/four-block-prefill/send.json | 11 + .../four-block-prefill/validation.json | 60 +++ .../2026-09-13/live-preload.go.txt | 510 ++++++++++++++++++ .../2026-09-13/local-race.txt | 11 + .../2026-09-13/local-unit.txt | 10 + .../2026-09-13/native-build.txt | 0 .../2026-09-13/native-race.txt | 11 + .../2026-09-13/native-vet.txt | 0 .../2026-09-13/round-1-baseline.txt | 7 + .../2026-09-13/round-1-candidate.txt | 8 + .../2026-09-13/round-2-baseline.txt | 7 + .../2026-09-13/round-2-candidate.txt | 8 + .../2026-09-13/round-3-baseline.txt | 7 + .../2026-09-13/round-3-candidate.txt | 8 + .../2026-09-13/run-native.sh | 31 ++ .../2026-09-13/service-after.txt | 2 + .../2026-09-13/service-before.txt | 2 + .../2026-09-13/source-sha256.txt | 2 + .../2026-09-13/summarize.py | 20 + .../2026-09-13/summary.json | 136 +++++ .../2026-09-13/topology.txt | 17 + 34 files changed, 1415 insertions(+) create mode 100644 docs/leader_block_packing.md create mode 100644 docs/results/leader-block-packing/2026-09-13/README.md create mode 100644 docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json create mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json create mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/metadata.json create mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/send.json create mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/validation.json create mode 100644 docs/results/leader-block-packing/2026-09-13/binary-sha256.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/evidence-manifest.json create mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json create mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json create mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json create mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json create mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json create mode 100644 docs/results/leader-block-packing/2026-09-13/live-preload.go.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/local-race.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/local-unit.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/native-build.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/native-race.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/native-vet.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/run-native.sh create mode 100644 docs/results/leader-block-packing/2026-09-13/service-after.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/service-before.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/source-sha256.txt create mode 100644 docs/results/leader-block-packing/2026-09-13/summarize.py create mode 100644 docs/results/leader-block-packing/2026-09-13/summary.json create mode 100644 docs/results/leader-block-packing/2026-09-13/topology.txt diff --git a/docs/leader_block_packing.md b/docs/leader_block_packing.md new file mode 100644 index 000000000..c650221e7 --- /dev/null +++ b/docs/leader_block_packing.md @@ -0,0 +1,94 @@ +# Leader block packing and synthetic load tests + +This change reduces work performed during a leader's available packing window. +The scheduler owns and decodes packet bytes once, and prepares static message +validation, instructions/account metadata, compute limits, message hash and cost +while transactions are queued. Each bank checks the immutable feature snapshot +before reuse. Account state, age, duplicates, strict fee-payer eligibility, rent, +execution and all resource budgets remain bank-dependent checks. + +Leader execution reuses borrowed-account scratch and skips detailed replay +stage timers. Missing-current-bank lookup errors defer base58 formatting until +used, avoiding wasted work before parent lookup. Entry Merkle hashing retains +only the root-building scratch, and queue removal avoids heap interface dispatch +and unused index updates while preserving priority/FIFO order. + +## Capacity and protocol limits + +Live banks use slot-dependent budgets with slot-time feature gates taking effect +in the epoch after activation. For the September 13 cluster's 200ms regime with +RaiseBlockLimitsTo100m, the budget was 50M block-cost units, 20M writable-account +cost units, 50MB allocated-data growth and 10MiB entry bytes. The entry packer +reserves 48 bytes for the ending tick. A 100M per-slot budget would be incorrect +in this regime. Active limits are logged when a leader bank opens. + +The readonly-pair fixture has one signature, one writable fee payer, two existing +readonly accounts, no instructions, and 198 wire bytes. Its observed actual cost +is 1,028 units: 720 signature, 300 write lock and eight loaded-account units. Its +program execution cost is zero. The theoretical cost-only ceiling is 48,638, +but upfront admission must fit the larger estimated loaded-data reservation: +the offline bank test includes 48,622 before rejecting the next transaction. +This workload is designed for signature/packing load, not application execution. + +## Reproduce locally + +All commands below are offline. They use deterministic test keys and in-memory +accounts; no RPC, faucet, funding or transaction submission occurs. + +```sh +# The 200,000-message, eight-payer, two-blockhash workload used to prefill four slots. +go test ./pkg/tpu/txfixture -run '^TestReadonlyPair200KDistinctMessages$' -count=1 + +# Fill a 50M-cost bank, reject the next tx, check fees, and round-trip all entries +# through actual shred generation and decoding, preserving transaction order/hash. +go test ./pkg/blockprod -run '^TestReadonlyPairBlockCapacityAndShredRoundTrip$' -count=1 + +# Whole-bank construction: one serial caller; pre-signed unique transactions. +GOMAXPROCS=8 go test ./pkg/blockprod -run '^$' \ + -bench '^BenchmarkReadonlyPair(FullBlock|PreparedFullBlock)$' -benchtime=3x -count=3 + +# More representative instruction workloads and smaller component microbenchmarks. +GOMAXPROCS=8 go test ./pkg/blockprod -run '^$' \ + -bench '^BenchmarkWorkingBank(Decoded|Prepared)?HotAccounts$' -benchtime=2s -count=3 +``` + +The whole-bank benchmark admits 48,622 transactions and includes execution, +account publication, entry batching/hash work and final entry flush. Signing and +bank setup are excluded. The prepared variant additionally does static +preparation before timing, modeling a queue ready before leadership. That work +is moved, not eliminated. Actual AccountsDB, signature verification, network, +consensus and the protocol deadline are outside this benchmark. The correctness +test's shred round-trip is also outside the timed benchmark. + +`txfixture.ReadonlyPairWire` provides the same ordered-pair construction as the +live experiment: 128×127 unique messages per payer/blockhash. Repeated ordinals +need a different payer or blockhash. The 200k test verifies uniqueness across +all messages and decodes/verifies representative signatures and phase boundaries. + +## Queue supply and completion reserve + +```toml +[validator] +tpu_max_buffered_transactions = 0 # default 131072 +block_completion_reserve_ms = 0 # default 75ms +``` + +The corresponding flags are `--tpu-max-buffered-transactions` and +`--leader-completion-reserve-ms`. The measured full-prefill trial used 262,144 +queue entries and a 60ms reserve. Those are opt-in tuning values; defaults stay +unchanged. A larger queue uses additional memory for owned wire, decoded and +prepared objects. A shorter reserve needs measured local finalization/broadcast +margin and does not change the protocol deadline. Shifting completion also +shifts later bank start times, so it does not add the same packing time to all +four blocks. + +The live results showed why total supply and timing matter: 120k transactions +cannot fill four approximately 48.6k blocks. An 80k refill competed with ongoing +work. Preloading 200k into a larger queue improved the observed four-block total, +while the first block still had less usable time and later banks awaited local +replay/adoption. These observations do not isolate a single CPU bottleneck. + +[Measured results, exact baselines and evidence](results/leader-block-packing/2026-09-13/README.md) +include both the successful near-limit block and the still-underfilled four-slot +window. The archived live helper is historical experiment source with explicit +cluster/identity/path constants; the offline fixture is the portable reproduction. diff --git a/docs/results/leader-block-packing/2026-09-13/README.md b/docs/results/leader-block-packing/2026-09-13/README.md new file mode 100644 index 000000000..06e270cb6 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/README.md @@ -0,0 +1,129 @@ +# Leader block packing: standalone PR and live experiment results + +Goal: keep Mithril's own 200ms leader slots supplied and reduce work in the +packing window, while enforcing the actual cluster cost/byte limits. This PR +extracts the live-tested leader changes onto current `alpenglow-dev` at +`33dde4050d9250557583395810799aaac2f54017`. It can be reviewed independently of +FEC PR #259 and the streaming transaction-verification follow-up. + +## Fresh comparison against current alpenglow-dev + +Ryzen 7 9700X (8 physical cores/16 threads), Go 1.26.4, GOMAXPROCS=8, affinity +CPUs 0–7, nice 10. One serial benchmark caller builds a bank containing **48,622 +unique 198-byte one-signature, zero-instruction transactions**. Both versions +use identical in-memory accounts, 50M block-cost/20M writable-account budgets, +10MiB entry-byte limits and the same fixture/benchmark source. The copied test +harness is the only addition to the baseline; production code is unchanged. +Three paired rounds alternate order (base/PR, PR/base, base/PR); each reported +sample times three full banks. Tables show medians of the three sample means. +The validator continued running, so normal shared-host variation remains. + +| Bank input path | alpenglow-dev | PR | Allocated MB/block, base → PR | +|---|---:|---:|---:| +| Wire bytes | 209.64 ms | **150.30 ms** | 259.27 → 223.45 | +| Already decoded | 189.73 ms | **132.40 ms** | 237.88 → 202.06 | +| Already decoded and statically prepared | — | **101.92 ms** | — → 173.67 | + +The like-for-like wire path takes **28.3% less bank time**; the decoded path +30.2% less. The prepared bank phase additionally moves static work before +leadership. It does not eliminate that work from total CPU use. Its three +samples were 121.71, 101.92 and 101.60 ms. No whole-validator 2× claim is made. + +Timing includes bank admission, execution, account publication, entry building, +Merkle hashing and final entry flush. It excludes signing, signature verification, +bank setup, actual AccountsDB, network/shred broadcast, consensus and replay +adoption. Thus this is not a promise to fit the same block inside every live +packing window. The offline correctness test independently round-trips all +emitted entries through shred generation and decoding, verifies fees/hash/order, +and rejects the next transaction at the block-cost boundary. + +`run-native.sh` records the commands and baseline harness copying. Source and +binary hashes, topology, race/vet/build logs and every paired measurement are +included. `python3 summarize.py` rebuilds summary.json. The source archive was +captured before documentation-only additions; its Go/module files match the +published implementation. The live validator's PID stayed unchanged. + +## What enabled the live result + +- Decode owned TPU bytes once. Prepare immutable static transaction data before + leadership; recheck feature compatibility and bank-dependent state on use. +- Reuse borrowed-account scratch during serialized bank execution, and omit + detailed replay timing from that leader hot path. +- Format missing-bank-account diagnostics lazily before falling back to parent + accounts; calculate writable keys once and avoid base58 loader comparisons. +- Calculate signature Merkle roots without retaining unused proof nodes. +- Remove scheduler heap dispatch/index-store overhead, preserving priority/FIFO. +- Expose bounded queue capacity and local completion reserve as tuning settings, + keeping the 131,072-entry and 75ms defaults. +- Correct slot-time-dependent resource budgets. In the tested 200ms regime, + RaiseBlockLimitsTo100m means **50M**, not 100M per block. The skipped initial + 42,827-transaction compute-budget block exceeded the real limit and is not a + successful result. After correction, 42,338 such transactions finalized at + 49,916,502 cost units. The lighter readonly-pair workload then approached 48.6k. + +Slot-budget reference: [Agave release 2e10d67 slot parameters](https://github.com/anza-xyz/agave/blob/2e10d67f909c23a9586e1b3d099f7b8ef810ccfe/runtime/src/slot_params.rs), +including following-epoch activation and scaling of both account and block costs. + +## Live results: separate from the standalone benchmark + +The live binaries combined these changes with FEC/producer and streaming +verification work from trial `15ad1845`. No new live traffic or deployment was +performed while preparing this PR. These sequential windows are observations, +not controlled attribution of each optimization. + +**Best single finalized block:** slot **3073657**, **48,523 transactions**, one +signature each, zero instructions/execution CU, zero failures, **49,881,644 / +50,000,000 block-cost units**. Its local completion deadline margin was 49ms. +RPC blockhash: `H6iU43SrXp94LRvfpvhLDskboUJA4CupsEVFJcCRLANF`. + +The cost-only ceiling is floor(50,000,000 / 1,028) = 48,638. Upfront loaded-data +reservation reduces the offline admission limit to 48,622; runtime timing and +background work can reduce live inclusion further. The best live block is 99 +transactions below that offline admission count. We did not reach 50,000. + +The later fully preloaded four-block test offered 200,000 transactions at 150k +transactions/sec before our leader window, with queue capacity 262,144 and a +60ms completion reserve. The four finalized blocks were: + +| Slot | Included transactions | Cost units | Completion margin | +|---|---:|---:|---:| +| 3077076 | 26,687 | 27,434,236 | 39ms | +| 3077077 | 42,150 | 43,330,200 | 45ms | +| 3077078 | 41,387 | 42,545,836 | 41ms | +| 3077079 | 42,607 | 43,799,996 | 41ms | + +Total **152,831**, all successful; the other 47,169 expired without inclusion. +The preceding timely-refill run included 124,988. The 22.3% observed increase +coincides with different queue size, prefill timing, process age and live +conditions; it is not an isolated software speedup. **Four maximum-size blocks +were not achieved.** First-block packing time was shorter; later starts waited +for local replay/adoption. A reserve reduction does not add an equal amount of +packing time to every slot. Buffered log output was too delayed to be a useful +refill trigger, so the subsequent test used RPC slot observations and finally +full prefill. + +Small extracted finalized-validation records are in best-block/ and +four-block-prefill/. evidence-manifest.json hashes the original source records. +Full prior evidence archives remain identified by SHA-256: + +- Best-block experiment: `f785aeaad00d5cc85e868bf1488e4ebb7adb0346f97308f0fc6d8ae538c943a5`. +- Four-block prefill: `e7a3f03b5f88f4110c9dc58d306f14142bee4c5ba2f33b5b348d5ea05ded525f`. + +`live-preload.go.txt` is the original bounded live sender source (no key material). +Its RPC/genesis/identity/path constants are historical experiment settings, not +portable defaults. The offline tests reproduce its message construction and +capacity checks without funds or network. See [reproduction commands](../../../leader_block_packing.md). + +## Validation and remaining work + +Local and native Zen 5 race suites passed for accounts, blockprod/scheduler, +costmodel, merkletree, replay, fees, turbine, txfixture, config and node. Vet and +full Mithril builds passed on both machines. The full repository suite is not +claimed to pass: prior baseline/candidate trials reproduced unrelated BPF-loader +failures. Tests cover owned packet lifetimes, stale feature preparation, +current payer-state changes, fees-only/instruction failures, duplicates/age, +arena reuse, queue ordering/eviction, Merkle parity and slot-budget activation. + +The discarded status-map allocation experiment showed no clear timing gain and +is not included. Reusing message hashes through local replay and shortening +replay/adoption transitions remain possible follow-ups, not completed gains. diff --git a/docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt b/docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt new file mode 100644 index 000000000..fb360e970 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt @@ -0,0 +1 @@ +ok github.com/Overclock-Validator/mithril/pkg/blockprod 1.076s diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json b/docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json new file mode 100644 index 000000000..d635f286e --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json @@ -0,0 +1,56 @@ +{ + "stage": "window-fill", + "finalized_load_transactions": 200000, + "not_included_after_expiry": 0, + "blocks": [ + { + "slot": 3073656, + "transactions": 31732, + "load_transactions": 31732, + "cost_units": 32620496, + "failed_transactions": 0 + }, + { + "slot": 3073657, + "transactions": 48523, + "load_transactions": 48523, + "cost_units": 49881644, + "failed_transactions": 0 + }, + { + "slot": 3073658, + "transactions": 39911, + "load_transactions": 39745, + "cost_units": 41103706, + "failed_transactions": 0 + }, + { + "slot": 3073659, + "transactions": 138, + "load_transactions": 0, + "cost_units": 204378, + "failed_transactions": 0 + }, + { + "slot": 3073740, + "transactions": 34188, + "load_transactions": 34150, + "cost_units": 35162478, + "failed_transactions": 0 + }, + { + "slot": 3073741, + "transactions": 45094, + "load_transactions": 45094, + "cost_units": 46356632, + "failed_transactions": 0 + }, + { + "slot": 3073742, + "transactions": 852, + "load_transactions": 756, + "cost_units": 919344, + "failed_transactions": 0 + } + ] +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json b/docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json new file mode 100644 index 000000000..85a32860b --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json @@ -0,0 +1,16 @@ +{ + "slot": 3073657, + "transactions": 48523, + "load_transactions": 48523, + "block_cost_units": 49881644, + "execution_cu": 0, + "failed_transactions": 0, + "load_cost_units_each": [ + 1028 + ], + "load_execution_cu_each": [ + 0 + ], + "finalized": true, + "blockhash": "H6iU43SrXp94LRvfpvhLDskboUJA4CupsEVFJcCRLANF" +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/metadata.json b/docs/results/leader-block-packing/2026-09-13/best-block/metadata.json new file mode 100644 index 000000000..d625573fc --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/best-block/metadata.json @@ -0,0 +1,17 @@ +{ + "blockhash": "4X7hVhH1RJReoEkJcFQSV2BTKbjFQT8JK16dD3bEEwuv", + "count": 200000, + "last_valid_block_height": 2839082, + "offered_tps": 150000, + "prefill_count": 120000, + "prefill_slots": 10, + "prepare_seconds": 2.715821878, + "sender": "127.0.0.1:8011; GOMAXPROCS=2; signatures prepared before send", + "shape": "readonly-pair", + "signatures_per_transaction": 1, + "topup_blockhash": "DTSw2jqMLS6A3yCuJ27d7BpH5sWttE53sy6Pw6FMwWie", + "topup_count": 80000, + "topup_trigger": "first local block broadcast", + "transaction_bytes": 198, + "window": 3073656 +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/send.json b/docs/results/leader-block-packing/2026-09-13/best-block/send.json new file mode 100644 index 000000000..4f4fd60e9 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/best-block/send.json @@ -0,0 +1,12 @@ +{ + "end_slot": 3073659, + "end_utc": "2026-09-13T06:45:12.217473403Z", + "first_error": "", + "not_sent": 0, + "send_errors": 0, + "send_seconds": 3.291255092, + "sent": 200000, + "start_utc": "2026-09-13T06:45:08.926218321Z", + "target_window": 3073656, + "topup_start_utc": "2026-09-13T06:45:11.683951471Z" +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/validation.json b/docs/results/leader-block-packing/2026-09-13/best-block/validation.json new file mode 100644 index 000000000..6af787f42 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/best-block/validation.json @@ -0,0 +1,99 @@ +{ + "utc": "2026-09-13T06:48:40.355016+00:00", + "stage": "window-fill", + "blocks": [ + { + "slot": 3073656, + "blockhash": "6CGAhXwTZSMsS5Uda6CD19VRqaYeBUQoodZJW7X9PXcx", + "alpenglow_block_id": "8xkgbbwhcXujEMbn1bKhkWvTEiEwBfoPZVkTNs4ihcgj", + "certificate_logs": [ + "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073656 rank=21 certificate=notarize block=8xkgbbwhcXujEMbn1bKhkWvTEiEwBfoPZVkTNs4ihcgj network_landed=8654" + ], + "finalized": true, + "finalization_ms": 7, + "deadline_margin_ms": 50, + "transactions": 31732, + "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073656 terminal=complete cause=complete reason=complete replay_frontier=3073651 live_slot=3073655 block=8xkgbbwhcXujEMbn1bKhkWvTEiEwBfoPZVkTNs4ihcgj parent_slot=3073651 txns=31732 window_elapsed_ms=149 finalization_ms=7 deadline_margin_ms=50 local_handoff_ms=0" + }, + { + "slot": 3073657, + "blockhash": "H6iU43SrXp94LRvfpvhLDskboUJA4CupsEVFJcCRLANF", + "alpenglow_block_id": "AQi4tKfzW3MRNHGhndxpL9sq6QBr5aoyR22ZPBA8RJUy", + "certificate_logs": [ + "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073657 rank=21 certificate=notarize block=AQi4tKfzW3MRNHGhndxpL9sq6QBr5aoyR22ZPBA8RJUy network_landed=8656" + ], + "finalized": true, + "finalization_ms": 9, + "deadline_margin_ms": 49, + "transactions": 48523, + "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073657 terminal=complete cause=complete reason=complete replay_frontier=3073656 live_slot=3073656 block=AQi4tKfzW3MRNHGhndxpL9sq6QBr5aoyR22ZPBA8RJUy parent_slot=3073656 txns=48523 window_elapsed_ms=350 finalization_ms=9 deadline_margin_ms=49 local_handoff_ms=0" + }, + { + "slot": 3073658, + "blockhash": "939M6Zfyuj9wwyrMVnzhF5WmyqzpSZfUQJwBA5gk2jto", + "alpenglow_block_id": "EWr2Nh8MFMUMQXvLsPAW6Z3xSPcuFP8efDrxjYtdz9s5", + "certificate_logs": [ + "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073658 rank=21 certificate=notarize-fallback block=EWr2Nh8MFMUMQXvLsPAW6Z3xSPcuFP8efDrxjYtdz9s5 network_landed=8658" + ], + "finalized": true, + "finalization_ms": 8, + "deadline_margin_ms": 49, + "transactions": 39911, + "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073658 terminal=complete cause=complete reason=complete replay_frontier=3073657 live_slot=3073657 block=EWr2Nh8MFMUMQXvLsPAW6Z3xSPcuFP8efDrxjYtdz9s5 parent_slot=3073657 txns=39911 window_elapsed_ms=550 finalization_ms=8 deadline_margin_ms=49 local_handoff_ms=0" + }, + { + "slot": 3073659, + "blockhash": "EXX1YCesL18q3KAUEehr4WsDZPcuSd2ErYKatMPBSDdS", + "alpenglow_block_id": "2JUsPbPn4JeyUhGkXqersNaYyKHZJut4NDgp6sFkarXe", + "certificate_logs": [ + "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073659 rank=21 certificate=notarize-fallback block=2JUsPbPn4JeyUhGkXqersNaYyKHZJut4NDgp6sFkarXe network_landed=8659" + ], + "finalized": true, + "finalization_ms": 10, + "deadline_margin_ms": 47, + "transactions": 138, + "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073659 terminal=complete cause=complete reason=complete replay_frontier=3073658 live_slot=3073658 block=2JUsPbPn4JeyUhGkXqersNaYyKHZJut4NDgp6sFkarXe parent_slot=3073658 txns=138 window_elapsed_ms=752 finalization_ms=10 deadline_margin_ms=47 local_handoff_ms=0" + }, + { + "slot": 3073740, + "blockhash": "saQYhwkvXUsuqa9xN2gE1cGMPZpAPJmB4fTZWYrQ6wf", + "alpenglow_block_id": "CzcvprhgxhmS4uU4Xxav9g68aBozMt9KTj22SFxF6fNi", + "certificate_logs": [ + "(+17m37s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073740 rank=21 certificate=notarize block=CzcvprhgxhmS4uU4Xxav9g68aBozMt9KTj22SFxF6fNi network_landed=8804" + ], + "finalized": true, + "finalization_ms": 10, + "deadline_margin_ms": 49, + "transactions": 34188, + "log": "(+17m37s) ALPENGLOW block production: broadcast local leader slot=3073740 terminal=complete cause=complete reason=complete replay_frontier=3073735 live_slot=3073739 block=CzcvprhgxhmS4uU4Xxav9g68aBozMt9KTj22SFxF6fNi parent_slot=3073735 txns=34188 window_elapsed_ms=150 finalization_ms=10 deadline_margin_ms=49 local_handoff_ms=0" + }, + { + "slot": 3073741, + "blockhash": "BN1bpbetrHiU2tzm4DCam4T9F8ZisDkgDPnqA35S9gFi", + "alpenglow_block_id": "146JxvWuGXKR1wvsA4vmosPDMNVoEBEoD93u6fv1abWX", + "certificate_logs": [ + "(+17m37s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073741 rank=21 certificate=notarize block=146JxvWuGXKR1wvsA4vmosPDMNVoEBEoD93u6fv1abWX network_landed=8806" + ], + "finalized": true, + "finalization_ms": 10, + "deadline_margin_ms": 49, + "transactions": 45094, + "log": "(+17m37s) ALPENGLOW block production: broadcast local leader slot=3073741 terminal=complete cause=complete reason=complete replay_frontier=3073740 live_slot=3073740 block=146JxvWuGXKR1wvsA4vmosPDMNVoEBEoD93u6fv1abWX parent_slot=3073740 txns=45094 window_elapsed_ms=350 finalization_ms=10 deadline_margin_ms=49 local_handoff_ms=0" + }, + { + "slot": 3073742, + "blockhash": "AFytPNQRtXwRbWwnzi6D9aAVz55h43KknkHTJUWnqtr7", + "alpenglow_block_id": "3STHfQmzAywmCDRJuosJ6U6ASHJZxeXpRxaBnTfhiSEb", + "certificate_logs": [ + "(+17m37s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073742 rank=21 certificate=notarize block=3STHfQmzAywmCDRJuosJ6U6ASHJZxeXpRxaBnTfhiSEb network_landed=8808" + ], + "finalized": true, + "finalization_ms": 9, + "deadline_margin_ms": 48, + "transactions": 852, + "log": "(+17m37s) ALPENGLOW block production: broadcast local leader slot=3073742 terminal=complete cause=complete reason=complete replay_frontier=3073741 live_slot=3073741 block=3STHfQmzAywmCDRJuosJ6U6ASHJZxeXpRxaBnTfhiSEb parent_slot=3073741 txns=852 window_elapsed_ms=551 finalization_ms=9 deadline_margin_ms=48 local_handoff_ms=0" + } + ], + "log_errors": [], + "included": 200000 +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/binary-sha256.txt b/docs/results/leader-block-packing/2026-09-13/binary-sha256.txt new file mode 100644 index 000000000..f9451ea62 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/binary-sha256.txt @@ -0,0 +1,3 @@ +302484874e65f2b01265ecf1f7c4ea3f8e850077844e795ef2a6bea55749ef6c /srv/mithril-leader-packing-pr-20260913/results/baseline.test +02395f246f80ce6a3e337000dc030dec1e04daf35a7149db4481f20587f406dc /srv/mithril-leader-packing-pr-20260913/results/candidate.test +230f6474e438ef29623537bef8aec6d5fa9fbb14014313231f132bf8e78a445f /srv/mithril-leader-packing-pr-20260913/results/mithril diff --git a/docs/results/leader-block-packing/2026-09-13/evidence-manifest.json b/docs/results/leader-block-packing/2026-09-13/evidence-manifest.json new file mode 100644 index 000000000..6091e4c10 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/evidence-manifest.json @@ -0,0 +1,52 @@ +[ + { + "source": "leader-window-fill-20260913/evidence/load/window-fill/validation.json", + "path": "best-block/validation.json", + "sha256": "a6c503c254c76138da8142c81cee665f31338f6bff756c77c4e9f5b85229b1c5" + }, + { + "source": "leader-window-fill-20260913/evidence/load/window-fill/largest-block-metrics.json", + "path": "best-block/largest-block-metrics.json", + "sha256": "733203a4bb62a7fd28f92b73ff0bdfb6592855d9aef2ece3f4447b4f45ac7c54" + }, + { + "source": "leader-window-fill-20260913/evidence/load/window-fill/all-finalized-costs.json", + "path": "best-block/all-finalized-costs.json", + "sha256": "f310f47b66bf49d14272109c3a97dc9c5f73ee740edac67aea783a38cffbf897" + }, + { + "source": "leader-window-fill-20260913/evidence/load/window-fill/metadata.json", + "path": "best-block/metadata.json", + "sha256": "265ca56fd39ff78b8d4ff2f2643f4e5400db69b4b32e6ffc5e027d3dbe3a1203" + }, + { + "source": "leader-window-fill-20260913/evidence/load/window-fill/send.json", + "path": "best-block/send.json", + "sha256": "0ebdc42dd32bee8085f1147512546b7aef851cd0a6e7e03cb45d0c22a818ae05" + }, + { + "source": "leader-window-prefill-20260913/evidence/load/window-fill/validation.json", + "path": "four-block-prefill/validation.json", + "sha256": "2223cb7bdf044ff9e23c6b08cfb40c1f428010de0f591322035b4574733567d0" + }, + { + "source": "leader-window-prefill-20260913/evidence/load/window-fill/largest-block-metrics.json", + "path": "four-block-prefill/largest-block-metrics.json", + "sha256": "8f3d6c54334eb3a0fec8568425ebe4895966ca756534fc5c8bbb6f40ab6af20e" + }, + { + "source": "leader-window-prefill-20260913/evidence/load/window-fill/all-finalized-costs.json", + "path": "four-block-prefill/all-finalized-costs.json", + "sha256": "3f46aa6eb8fe6e0d6d738a7cc7026780ae4135bd1bee54f5a13ef3a5f6ccd14c" + }, + { + "source": "leader-window-prefill-20260913/evidence/load/window-fill/metadata.json", + "path": "four-block-prefill/metadata.json", + "sha256": "2e6c576474cd573124700a9abb8b40429d8c14092259ad50cf47c3d4bce0dea4" + }, + { + "source": "leader-window-prefill-20260913/evidence/load/window-fill/send.json", + "path": "four-block-prefill/send.json", + "sha256": "cddce7d90b8d039b06167ad5d845a0a0388fe678f95f5775bd08e9dd314643b9" + } +] diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json new file mode 100644 index 000000000..ed4e70971 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json @@ -0,0 +1,35 @@ +{ + "stage": "window-fill", + "finalized_load_transactions": 152831, + "not_included_after_expiry": 47169, + "blocks": [ + { + "slot": 3077076, + "transactions": 26687, + "load_transactions": 26687, + "cost_units": 27434236, + "failed_transactions": 0 + }, + { + "slot": 3077077, + "transactions": 42150, + "load_transactions": 42150, + "cost_units": 43330200, + "failed_transactions": 0 + }, + { + "slot": 3077078, + "transactions": 41387, + "load_transactions": 41387, + "cost_units": 42545836, + "failed_transactions": 0 + }, + { + "slot": 3077079, + "transactions": 42607, + "load_transactions": 42607, + "cost_units": 43799996, + "failed_transactions": 0 + } + ] +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json new file mode 100644 index 000000000..22e907bc6 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json @@ -0,0 +1,16 @@ +{ + "slot": 3077079, + "transactions": 42607, + "load_transactions": 42607, + "block_cost_units": 43799996, + "execution_cu": 0, + "failed_transactions": 0, + "load_cost_units_each": [ + 1028 + ], + "load_execution_cu_each": [ + 0 + ], + "finalized": true, + "blockhash": "FXcMHsNzG53SnuW1kR2ZzxGLx6p9FxeUsgeLvMzS1rhL" +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json new file mode 100644 index 000000000..1a321b46d --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json @@ -0,0 +1,17 @@ +{ + "blockhash": "Cjef4Kov7MMcdEeY2vUrfT2pineEzykVvsy6321CPbtE", + "count": 200000, + "last_valid_block_height": 2842026, + "offered_tps": 150000, + "prefill_count": 200000, + "prefill_slots": 20, + "prepare_seconds": 2.571031937, + "sender": "127.0.0.1:8011; GOMAXPROCS=2; signatures prepared before send", + "shape": "readonly-pair", + "signatures_per_transaction": 1, + "topup_blockhash": "GUgh5AT5Ci7p2Et6GzZFscesHLQ7gVmmpHyzhuutcvQx", + "topup_count": 0, + "topup_trigger": "none; entire workload preloaded", + "transaction_bytes": 198, + "window": 3077076 +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json new file mode 100644 index 000000000..2656f8f7f --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json @@ -0,0 +1,11 @@ +{ + "end_slot": 3077062, + "end_utc": "2026-09-13T06:57:13.621742904Z", + "first_error": "", + "not_sent": 0, + "send_errors": 0, + "send_seconds": 1.333626813, + "sent": 200000, + "start_utc": "2026-09-13T06:57:12.288116091Z", + "target_window": 3077076 +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json new file mode 100644 index 000000000..d571065a2 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json @@ -0,0 +1,60 @@ +{ + "utc": "2026-09-13T06:58:15.837757+00:00", + "stage": "window-fill", + "blocks": [ + { + "slot": 3077076, + "blockhash": "6WQ6hRYS9KLCsAWcvvGuwRujFL8uUH6AX28wUrxCAPfw", + "alpenglow_block_id": "A3cMotBW2NT7LaGk1NgEQ7RinuWAeVwNiAeCZQ2GYpYA", + "certificate_logs": [ + "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077076 rank=21 certificate=notarize block=A3cMotBW2NT7LaGk1NgEQ7RinuWAeVwNiAeCZQ2GYpYA network_landed=553" + ], + "finalized": true, + "finalization_ms": 15, + "deadline_margin_ms": 39, + "transactions": 26687, + "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077076 terminal=complete cause=complete reason=complete replay_frontier=3077075 live_slot=3077077 block=A3cMotBW2NT7LaGk1NgEQ7RinuWAeVwNiAeCZQ2GYpYA parent_slot=3077075 txns=26687 window_elapsed_ms=160 finalization_ms=15 deadline_margin_ms=39 local_handoff_ms=0" + }, + { + "slot": 3077077, + "blockhash": "A4DT5t7mxKQ7Udb4ixU7dLiww9feWkPHswGzGyFCe5at", + "alpenglow_block_id": "8tMUnRr5rik2yiKdwxJnKyLLDsc5vyn6Q9vT7jazvvsX", + "certificate_logs": [ + "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077077 rank=21 certificate=notarize block=8tMUnRr5rik2yiKdwxJnKyLLDsc5vyn6Q9vT7jazvvsX network_landed=555" + ], + "finalized": true, + "finalization_ms": 14, + "deadline_margin_ms": 45, + "transactions": 42150, + "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077077 terminal=complete cause=complete reason=complete replay_frontier=3077076 live_slot=3077076 block=8tMUnRr5rik2yiKdwxJnKyLLDsc5vyn6Q9vT7jazvvsX parent_slot=3077076 txns=42150 window_elapsed_ms=354 finalization_ms=14 deadline_margin_ms=45 local_handoff_ms=0" + }, + { + "slot": 3077078, + "blockhash": "A48aPKzQzo5P7TN4qWn3hv1CwK8yRLvsCXPXnc3J6Yyj", + "alpenglow_block_id": "5FhWEtjG9WGj11z1ZkD5Gx3jYAkVaPytx1PPacQ6bR4H", + "certificate_logs": [ + "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077078 rank=21 certificate=notarize block=5FhWEtjG9WGj11z1ZkD5Gx3jYAkVaPytx1PPacQ6bR4H network_landed=557" + ], + "finalized": true, + "finalization_ms": 13, + "deadline_margin_ms": 41, + "transactions": 41387, + "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077078 terminal=complete cause=complete reason=complete replay_frontier=3077077 live_slot=3077077 block=5FhWEtjG9WGj11z1ZkD5Gx3jYAkVaPytx1PPacQ6bR4H parent_slot=3077077 txns=41387 window_elapsed_ms=558 finalization_ms=13 deadline_margin_ms=41 local_handoff_ms=0" + }, + { + "slot": 3077079, + "blockhash": "FXcMHsNzG53SnuW1kR2ZzxGLx6p9FxeUsgeLvMzS1rhL", + "alpenglow_block_id": "ChYL4dneUd22GQc3YxFafh7Lc1XvEdCTFPLLaAhAVDDQ", + "certificate_logs": [ + "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077079 rank=21 certificate=notarize-fallback block=ChYL4dneUd22GQc3YxFafh7Lc1XvEdCTFPLLaAhAVDDQ network_landed=559" + ], + "finalized": true, + "finalization_ms": 13, + "deadline_margin_ms": 41, + "transactions": 42607, + "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077079 terminal=complete cause=complete reason=complete replay_frontier=3077078 live_slot=3077078 block=ChYL4dneUd22GQc3YxFafh7Lc1XvEdCTFPLLaAhAVDDQ parent_slot=3077078 txns=42607 window_elapsed_ms=758 finalization_ms=13 deadline_margin_ms=41 local_handoff_ms=0" + } + ], + "log_errors": [], + "included": 152831 +} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/live-preload.go.txt b/docs/results/leader-block-packing/2026-09-13/live-preload.go.txt new file mode 100644 index 000000000..ca979fa37 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/live-preload.go.txt @@ -0,0 +1,510 @@ +// Bounded, own-validator-only Alpenglow load experiment. Not production code. +package main + +import ( + "bytes" + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/Overclock-Validator/mithril/pkg/tpu/quicserver/testutils" + "github.com/gagliardetto/solana-go" +) + +const identity = "AzDzG3imFz5ihvetCcGixiwuieH7jfsaivnGDXgZo6Z8" +const genesis = "D5SkuirXeXeUTR7z2A65nzgU1GZ6uJ4vzf2tCy3Zt88b" +const rpcURL = "https://rpc.ag.validator1.net" + +var root = flag.String("root", "/srv/mithril-leader-window-prefill-20260913/load", "private experiment directory") +var mode = flag.String("mode", "selfcheck", "init, selfcheck, simulate, or run") +var count = flag.Int("count", 32, "unique transactions; hard limit 200000") +var rate = flag.Int("rate", 100, "offered transactions/s; hard limit 150000") +var stage = flag.String("stage", "pilot", "unique result directory name") +var prefill = flag.Int("prefill-slots", 0, "send this many slots before leadership (0..20)") +var shape = flag.String("shape", "readonly-pair", "account-nonce, compute-budget or readonly-pair") +var transport = &http.Client{Timeout: 15 * time.Second} + +func must(err error) { + if err != nil { + panic(err) + } +} +func logEvent(v any) { b, e := json.Marshal(v); must(e); fmt.Println(string(b)) } +func save(path string, v any) { + b, e := json.MarshalIndent(v, "", " ") + must(e) + must(os.WriteFile(path, b, 0600)) +} +func rpc(method string, params any, out any) error { + b, e := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": method, "params": params}) + if e != nil { + return e + } + r, e := transport.Post(rpcURL, "application/json", bytes.NewReader(b)) + if e != nil { + return e + } + defer r.Body.Close() + var env struct { + Result json.RawMessage `json:"result"` + Error json.RawMessage `json:"error"` + } + if e = json.NewDecoder(r.Body).Decode(&env); e != nil { + return e + } + if len(env.Error) > 0 && string(env.Error) != "null" { + return fmt.Errorf("RPC %s: %s", method, env.Error) + } + return json.Unmarshal(env.Result, out) +} +func keyPath(i int) string { return filepath.Join(*root, "keys", fmt.Sprintf("payer-%d.json", i)) } +func initKeys() { + must(os.MkdirAll(filepath.Join(*root, "keys"), 0700)) + var pubs []string + for i := 0; i < 8; i++ { + _, p, e := ed25519.GenerateKey(rand.Reader) + must(e) + vals := make([]int, len(p)) + for j, b := range p { + vals[j] = int(b) + } + b, e := json.Marshal(vals) + must(e) + f, e := os.OpenFile(keyPath(i), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + must(e) + _, e = f.Write(b) + must(e) + must(f.Close()) + pubs = append(pubs, solana.PublicKeyFromBytes(p.Public().(ed25519.PublicKey)).String()) + } + save(filepath.Join(*root, "payers.json"), pubs) + logEvent(map[string]any{"payers": pubs}) +} +func loadKeys() []ed25519.PrivateKey { + keys := make([]ed25519.PrivateKey, 8) + for i := range keys { + b, e := os.ReadFile(keyPath(i)) + must(e) + var a []int + must(json.Unmarshal(b, &a)) + if len(a) != 64 { + panic("invalid key length") + } + p := make([]byte, 64) + for j, v := range a { + if v < 0 || v > 255 { + panic("invalid key byte") + } + p[j] = byte(v) + } + keys[i] = ed25519.PrivateKey(p) + } + return keys +} +func pairIndices(ordinal int) (int, int) { + if ordinal < 0 || ordinal >= 128*127 { + panic("pair ordinal outside pool capacity") + } + // A permutation distributes early transactions over the fixed account pool. + n := (ordinal * 7919) % (128 * 127) + a, b := n/127, n%127 + if b >= a { + b++ + } + return a, b +} +func loadPool() []solana.PublicKey { + b, e := os.ReadFile(filepath.Join(*root, "pool.json")) + must(e) + var names []string + must(json.Unmarshal(b, &names)) + if len(names) != 128 { + panic("wrong pool length") + } + p := make([]solana.PublicKey, 128) + seen := map[solana.PublicKey]bool{} + for i, n := range names { + p[i] = solana.MustPublicKeyFromBase58(n) + if seen[p[i]] { + panic("duplicate pool account") + } + seen[p[i]] = true + } + return p +} +func makePairWire(key ed25519.PrivateKey, hash solana.Hash, pool []solana.PublicKey, ordinal int) []byte { + a, b := pairIndices(ordinal) + payer := solana.PublicKeyFromBytes(key.Public().(ed25519.PublicKey)) + if payer == pool[a] || payer == pool[b] { + panic("payer must be distinct from readonly pool") + } + msg := []byte{1, 0, 2, 3} + msg = append(msg, payer[:]...) + msg = append(msg, pool[a][:]...) + msg = append(msg, pool[b][:]...) + msg = append(msg, hash[:]...) + msg = append(msg, 0) + w := []byte{1} + w = append(w, ed25519.Sign(key, msg)...) + return append(w, msg...) +} + +func makeWire(key ed25519.PrivateKey, hash solana.Hash, nonce [32]byte) []byte { + // One writable signer, one unique readonly unsigned account, no instructions. + msg := []byte{1, 0, 1, 2} + msg = append(msg, key.Public().(ed25519.PublicKey)...) + msg = append(msg, nonce[:]...) + msg = append(msg, hash[:]...) + msg = append(msg, 0) + sig := ed25519.Sign(key, msg) + w := []byte{1} + w = append(w, sig...) + return append(w, msg...) +} +func makeBudgetWire(key ed25519.PrivateKey, hash solana.Hash, limit uint32) []byte { + program := solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111") + msg := []byte{1, 0, 1, 2} + msg = append(msg, key.Public().(ed25519.PublicKey)...) + msg = append(msg, program[:]...) + msg = append(msg, hash[:]...) + msg = append(msg, 1, 1, 0, 5, 2) // one instruction: program 1, no accounts, 5 data bytes, SetComputeUnitLimit + msg = binary.LittleEndian.AppendUint32(msg, limit) + w := []byte{1} + w = append(w, ed25519.Sign(key, msg)...) + return append(w, msg...) +} +func check(w []byte) { + t, e := solana.TransactionFromBytes(w) + must(e) + if (len(w) != 166 && len(w) != 174 && len(w) != 198) || len(t.Signatures) != 1 || len(t.Message.Instructions) > 1 { + panic("wrong transaction shape") + } + if !ed25519.Verify(ed25519.PublicKey(t.Message.AccountKeys[0][:]), w[65:], w[1:65]) { + panic("invalid signature") + } +} +func getSlot() uint64 { + var s uint64 + must(rpc("getSlot", []any{map[string]any{"commitment": "processed"}}, &s)) + return s +} +func getHash() (solana.Hash, uint64) { + var r struct { + Value struct { + Blockhash string + LastValidBlockHeight uint64 + } + } + must(rpc("getLatestBlockhash", []any{map[string]any{"commitment": "confirmed"}}, &r)) + h, e := solana.HashFromBase58(r.Value.Blockhash) + must(e) + return h, r.Value.LastValidBlockHeight +} +func selectWindow() uint64 { + var e struct{ AbsoluteSlot, SlotIndex uint64 } + must(rpc("getEpochInfo", []any{}, &e)) + var sched map[string][]uint64 + must(rpc("getLeaderSchedule", []any{nil, map[string]any{"identity": identity}}, &sched)) + base := e.AbsoluteSlot - e.SlotIndex + for _, s := range sched[identity] { + abs := base + s + if abs%4 == 0 && abs > e.AbsoluteSlot+65 { + return abs + } + } + panic("no suitable leader window in this epoch") +} +func awaitSlot(target uint64) uint64 { + deadline := time.Now().Add(10 * time.Minute) + for time.Now().Before(deadline) { + s := getSlot() + if s >= target { + return s + } + if target-s > 30 { + time.Sleep(2 * time.Second) + } else { + time.Sleep(75 * time.Millisecond) + } + } + panic("wait deadline") +} +func makePhasedPairWire(keys []ed25519.PrivateKey, first, second solana.Hash, pool []solana.PublicKey, i int) []byte { + if i < 0 || i >= 200000 || first == second { + panic("invalid phased load input") + } + txHash, ordinal := first, i/8 + if i >= 120000 { + txHash, ordinal = second, (i-120000)/8 + } + return makePairWire(keys[i%8], txHash, pool, ordinal) +} + +func main() { + flag.Parse() + if *shape != "readonly-pair" && *shape != "account-nonce" && *shape != "compute-budget" { + panic("invalid shape") + } + if *mode == "init" { + initKeys() + return + } + if *count < 1 || *count > 200000 || *rate < 1 || *rate > 150000 || *prefill < 0 || *prefill > 20 { + panic("outside experiment bounds") + } + if filepath.Base(*stage) != *stage { + panic("invalid stage") + } + if *mode == "selfcheck" { + _, p, e := ed25519.GenerateKey(rand.Reader) + must(e) + w := makeWire(p, solana.Hash{}, sha256.Sum256([]byte("test"))) + check(w) + check(makeBudgetWire(p, solana.Hash{}, 1000)) + pool := make([]solana.PublicKey, 128) + for i := range pool { + pool[i] = solana.PublicKey{byte(i + 1), 77} + } + seen := map[[64]byte]bool{} + for i := 0; i < 128*127; i++ { + a, b := pairIndices(i) + var pair [64]byte + copy(pair[:32], pool[a][:]) + copy(pair[32:], pool[b][:]) + if a == b || seen[pair] { + panic("pair collision") + } + seen[pair] = true + } + for _, i := range []int{0, 1, 126, 127, 14999, 16255} { + check(makePairWire(p, solana.Hash{}, pool, i)) + } + logEvent(map[string]any{"wire_bytes": 198, "signatures": 1, "signature_verified": true, "unique_pairs": len(seen)}) + return + } + var g string + must(rpc("getGenesisHash", []any{}, &g)) + if g != genesis { + panic("unexpected cluster genesis") + } + keys := loadKeys() + pool := loadPool() + if *mode == "simulate" { + h, _ := getHash() + w := makeWire(keys[0], h, sha256.Sum256([]byte("simulation"))) + if *shape == "compute-budget" { + w = makeBudgetWire(keys[0], h, 1000) + } + if *shape == "readonly-pair" { + w = makePairWire(keys[0], h, pool, 14999) + } + check(w) + var sim any + must(rpc("simulateTransaction", []any{base64.StdEncoding.EncodeToString(w), map[string]any{"encoding": "base64", "sigVerify": true, "commitment": "confirmed"}}, &sim)) + save(filepath.Join(*root, *shape+"-simulation.json"), sim) + logEvent(sim) + return + } + if *mode != "run" { + panic("unknown mode") + } + // Claim the stage before signing/sending. No automatic replay of uncertain runs. + out := filepath.Join(*root, *stage) + must(os.Mkdir(out, 0700)) + // Bound all experiment sends by a durable count, including uncertain prior runs. + ledger := filepath.Join(*root, "authorized-count.json") + used := 0 + if b, e := os.ReadFile(ledger); e == nil { + must(json.Unmarshal(b, &used)) + } else if !os.IsNotExist(e) { + must(e) + } + if used+*count > 200000 { + panic("cumulative maximum fee 1.0 testnet SOL exceeded") + } + save(ledger, used+*count) + window := selectWindow() + logEvent(map[string]any{"event": "waiting_to_prepare", "leader_window": window, "now_slot": getSlot(), "count": *count, "offered_tps": *rate}) + awaitSlot(window - 55) + hash, lastHeight := getHash() + hashLedger := filepath.Join(*root, "used-blockhashes.json") + usedHashes := map[string]bool{} + if data, err := os.ReadFile(hashLedger); err == nil { + must(json.Unmarshal(data, &usedHashes)) + } else if !os.IsNotExist(err) { + must(err) + } + if usedHashes[hash.String()] { + panic("blockhash already used by this experiment") + } + usedHashes[hash.String()] = true + save(hashLedger, usedHashes) + secondHash, secondHeight := hash, lastHeight + if *count > 120000 { + for attempts := 0; attempts < 40; attempts++ { + time.Sleep(100 * time.Millisecond) + secondHash, secondHeight = getHash() + if secondHash != hash { + break + } + } + if secondHash == hash || usedHashes[secondHash.String()] { + panic("no fresh top-up blockhash") + } + usedHashes[secondHash.String()] = true + save(hashLedger, usedHashes) + } + nonce := make([]byte, 32) + _, e := rand.Read(nonce) + must(e) + wires := make([][]byte, *count) + next := atomic.Int64{} + var wg sync.WaitGroup + beg := time.Now() + for k := 0; k < 4; k++ { + wg.Add(1) + go func() { + defer wg.Done() + for { + i := int(next.Add(1) - 1) + if i >= len(wires) { + return + } + n := append([]byte(nil), nonce...) + n = binary.LittleEndian.AppendUint64(n, uint64(i)) + wires[i] = makeWire(keys[i%8], hash, sha256.Sum256(n)) + if *shape == "compute-budget" { + wires[i] = makeBudgetWire(keys[i%8], hash, uint32(1000+i)) + } + if *shape == "readonly-pair" { + if len(wires) > 120000 { + wires[i] = makePhasedPairWire(keys, hash, secondHash, pool, i) + } else { + wires[i] = makePairWire(keys[i%8], hash, pool, i/8) + } + } + } + }() + } + wg.Wait() + check(wires[0]) + check(wires[len(wires)-1]) + if len(wires) > 120000 { + check(wires[120000]) + } + ids := make([]string, len(wires)) + for i, w := range wires { + ids[i] = solana.SignatureFromBytes(w[1:65]).String() + } + save(filepath.Join(out, "signatures.json"), ids) + meta := map[string]any{"window": window, "count": *count, "offered_tps": *rate, "transaction_bytes": len(wires[0]), "shape": *shape, "signatures_per_transaction": 1, "prefill_slots": *prefill, "blockhash": hash.String(), "last_valid_block_height": max(lastHeight, secondHeight), "topup_blockhash": secondHash.String(), "prefill_count": *count, "topup_count": 0, "topup_trigger": "none; entire workload preloaded", "prepare_seconds": time.Since(beg).Seconds(), "sender": "127.0.0.1:8011; GOMAXPROCS=2; signatures prepared before send"} + save(filepath.Join(out, "metadata.json"), meta) + if getSlot() >= window-uint64(*prefill) { + panic("missed preparation deadline; no sends") + } + client, e := testutils.NewClient() + must(e) + // Four connections and four streams each respect the validator's existing limits. + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + type senderFn func(context.Context, []byte) error + senders := make([]senderFn, 4) + var closers []func() + for i := range senders { + c, e := client.Dial(ctx, "127.0.0.1:8011") + must(e) + senders[i] = func(cctx context.Context, w []byte) error { return client.Send(cctx, c, w) } + closers = append(closers, func() { _ = c.CloseWithError(0, "") }) + } + defer func() { + for _, f := range closers { + f() + } + }() + observed := awaitSlot(window - uint64(*prefill)) + if observed > window+1 { + panic("missed send window; no sends") + } + var height uint64 + must(rpc("getBlockHeight", []any{map[string]any{"commitment": "confirmed"}}, &height)) + if height+10 >= lastHeight { + panic("blockhash too old") + } + logEvent(map[string]any{"event": "sending", "observed_slot": observed, "leader_window": window}) + start := time.Now() + jobs := make(chan int, 512) + var sent, errors, skipped atomic.Uint64 + var firstErr string + var errMu sync.Mutex + var endMu sync.Mutex + lastSend := start + sendCtx, stop := context.WithTimeout(context.Background(), 3*time.Second) + defer stop() + for k := 0; k < 16; k++ { + wg.Add(1) + go func(worker int) { + defer wg.Done() + for i := range jobs { + if sendCtx.Err() != nil { + skipped.Add(1) + continue + } + e := senders[worker%4](sendCtx, wires[i]) + if e != nil { + errors.Add(1) + errMu.Lock() + if firstErr == "" { + firstErr = e.Error() + } + errMu.Unlock() + } else { + sent.Add(1) + endMu.Lock() + lastSend = time.Now() + endMu.Unlock() + } + } + }(k) + } + for i := 0; i < len(wires); { + + if sendCtx.Err() != nil { + skipped.Add(uint64(len(wires) - i)) + break + } + end := i + 150 + if end > len(wires) { + end = len(wires) + } + target := start.Add(time.Duration(float64(i) / float64(*rate) * float64(time.Second))) + if d := time.Until(target); d > 0 { + time.Sleep(d) + } + for ; i < end; i++ { + select { + case jobs <- i: + case <-sendCtx.Done(): + skipped.Add(1) + } + } + } + close(jobs) + wg.Wait() + summary := map[string]any{"sent": sent.Load(), "send_errors": errors.Load(), "not_sent": skipped.Load(), "first_error": firstErr, "start_utc": start.UTC().Format(time.RFC3339Nano), "end_utc": lastSend.UTC().Format(time.RFC3339Nano), "send_seconds": lastSend.Sub(start).Seconds(), "end_slot": getSlot(), "target_window": window} + save(filepath.Join(out, "send.json"), summary) + logEvent(summary) + // Let already-written stream data be acknowledged before closing QUIC connections. + time.Sleep(500 * time.Millisecond) +} diff --git a/docs/results/leader-block-packing/2026-09-13/local-race.txt b/docs/results/leader-block-packing/2026-09-13/local-race.txt new file mode 100644 index 000000000..1a9f78b60 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/local-race.txt @@ -0,0 +1,11 @@ +ok github.com/Overclock-Validator/mithril/pkg/accounts 1.305s +ok github.com/Overclock-Validator/mithril/pkg/blockprod 21.817s +ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 1.949s +ok github.com/Overclock-Validator/mithril/pkg/costmodel 2.008s +ok github.com/Overclock-Validator/mithril/pkg/merkletree 2.233s +ok github.com/Overclock-Validator/mithril/pkg/replay 4.644s +ok github.com/Overclock-Validator/mithril/pkg/fees 1.961s +ok github.com/Overclock-Validator/mithril/pkg/turbine 5.114s +ok github.com/Overclock-Validator/mithril/pkg/tpu/txfixture 54.959s +ok github.com/Overclock-Validator/mithril/pkg/config 2.348s +ok github.com/Overclock-Validator/mithril/cmd/mithril/node 3.229s diff --git a/docs/results/leader-block-packing/2026-09-13/local-unit.txt b/docs/results/leader-block-packing/2026-09-13/local-unit.txt new file mode 100644 index 000000000..0cb4e1052 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/local-unit.txt @@ -0,0 +1,10 @@ +ok github.com/Overclock-Validator/mithril/pkg/accounts 0.223s +ok github.com/Overclock-Validator/mithril/pkg/blockprod 0.499s +ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 0.593s +ok github.com/Overclock-Validator/mithril/pkg/costmodel 0.816s +ok github.com/Overclock-Validator/mithril/pkg/merkletree 0.625s +ok github.com/Overclock-Validator/mithril/pkg/replay 1.666s +ok github.com/Overclock-Validator/mithril/pkg/fees 0.985s +ok github.com/Overclock-Validator/mithril/pkg/turbine 2.322s +ok github.com/Overclock-Validator/mithril/pkg/config 1.300s +ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.840s diff --git a/docs/results/leader-block-packing/2026-09-13/native-build.txt b/docs/results/leader-block-packing/2026-09-13/native-build.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/results/leader-block-packing/2026-09-13/native-race.txt b/docs/results/leader-block-packing/2026-09-13/native-race.txt new file mode 100644 index 000000000..30a569aaa --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/native-race.txt @@ -0,0 +1,11 @@ +ok github.com/Overclock-Validator/mithril/pkg/accounts 1.108s +ok github.com/Overclock-Validator/mithril/pkg/blockprod 14.102s +ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 1.384s +ok github.com/Overclock-Validator/mithril/pkg/costmodel 1.181s +ok github.com/Overclock-Validator/mithril/pkg/merkletree 1.364s +ok github.com/Overclock-Validator/mithril/pkg/replay 2.236s +ok github.com/Overclock-Validator/mithril/pkg/fees 1.135s +ok github.com/Overclock-Validator/mithril/pkg/turbine 3.135s +ok github.com/Overclock-Validator/mithril/pkg/tpu/txfixture 31.532s +ok github.com/Overclock-Validator/mithril/pkg/config 1.007s +ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.181s diff --git a/docs/results/leader-block-packing/2026-09-13/native-vet.txt b/docs/results/leader-block-packing/2026-09-13/native-vet.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt b/docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt new file mode 100644 index 000000000..8e75ec08d --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt @@ -0,0 +1,7 @@ +goos: linux +goarch: amd64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod +cpu: AMD Ryzen 7 9700X 8-Core Processor +BenchmarkReadonlyPairFullBlock/wire-8 3 209635460 ns/op 48622 tx/block 259271528 B/op 3363451 allocs/op +BenchmarkReadonlyPairFullBlock/decoded-8 3 188551991 ns/op 48622 tx/block 237879586 B/op 3071729 allocs/op +PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt b/docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt new file mode 100644 index 000000000..2b12f04aa --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt @@ -0,0 +1,8 @@ +goos: linux +goarch: amd64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod +cpu: AMD Ryzen 7 9700X 8-Core Processor +BenchmarkReadonlyPairFullBlock/wire-8 3 167507945 ns/op 48622 tx/block 223454984 B/op 2728489 allocs/op +BenchmarkReadonlyPairFullBlock/decoded-8 3 129990183 ns/op 48622 tx/block 202061266 B/op 2436756 allocs/op +BenchmarkReadonlyPairPreparedFullBlock-8 3 121710569 ns/op 48622 tx/block 173666002 B/op 1950536 allocs/op +PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt b/docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt new file mode 100644 index 000000000..9270847f3 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt @@ -0,0 +1,7 @@ +goos: linux +goarch: amd64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod +cpu: AMD Ryzen 7 9700X 8-Core Processor +BenchmarkReadonlyPairFullBlock/wire-8 3 206873746 ns/op 48622 tx/block 259272293 B/op 3363455 allocs/op +BenchmarkReadonlyPairFullBlock/decoded-8 3 214317833 ns/op 48622 tx/block 237880618 B/op 3071736 allocs/op +PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt b/docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt new file mode 100644 index 000000000..ba5529459 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt @@ -0,0 +1,8 @@ +goos: linux +goarch: amd64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod +cpu: AMD Ryzen 7 9700X 8-Core Processor +BenchmarkReadonlyPairFullBlock/wire-8 3 145202527 ns/op 48622 tx/block 223454946 B/op 2728489 allocs/op +BenchmarkReadonlyPairFullBlock/decoded-8 3 151023757 ns/op 48622 tx/block 202061384 B/op 2436757 allocs/op +BenchmarkReadonlyPairPreparedFullBlock-8 3 101921901 ns/op 48622 tx/block 173666021 B/op 1950536 allocs/op +PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt b/docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt new file mode 100644 index 000000000..ef23596b3 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt @@ -0,0 +1,7 @@ +goos: linux +goarch: amd64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod +cpu: AMD Ryzen 7 9700X 8-Core Processor +BenchmarkReadonlyPairFullBlock/wire-8 3 209864927 ns/op 48622 tx/block 259274824 B/op 3363464 allocs/op +BenchmarkReadonlyPairFullBlock/decoded-8 3 189729779 ns/op 48622 tx/block 237880589 B/op 3071738 allocs/op +PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt b/docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt new file mode 100644 index 000000000..a2279a516 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt @@ -0,0 +1,8 @@ +goos: linux +goarch: amd64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod +cpu: AMD Ryzen 7 9700X 8-Core Processor +BenchmarkReadonlyPairFullBlock/wire-8 3 150295927 ns/op 48622 tx/block 223455032 B/op 2728489 allocs/op +BenchmarkReadonlyPairFullBlock/decoded-8 3 132400481 ns/op 48622 tx/block 202061368 B/op 2436757 allocs/op +BenchmarkReadonlyPairPreparedFullBlock-8 3 101595082 ns/op 48622 tx/block 173666032 B/op 1950536 allocs/op +PASS diff --git a/docs/results/leader-block-packing/2026-09-13/run-native.sh b/docs/results/leader-block-packing/2026-09-13/run-native.sh new file mode 100644 index 000000000..8961a3110 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/run-native.sh @@ -0,0 +1,31 @@ +set -eu +review_dir=/srv/mithril-leader-packing-pr-20260913 +mkdir "$review_dir" +mkdir "$review_dir/candidate" "$review_dir/baseline" "$review_dir/results" +tar -xzf /tmp/mithril-leader-packing-pr-20260913.tar.gz -C "$review_dir/candidate" +tar -xf /tmp/mithril-leader-base-20260913.tar -C "$review_dir/baseline" +cp "$review_dir/candidate/pkg/blockprod/readonly_block_bench_test.go" "$review_dir/baseline/pkg/blockprod/" +cp "$review_dir/candidate/pkg/tpu/txfixture/readonly_pair.go" "$review_dir/baseline/pkg/tpu/txfixture/" +export PATH=/usr/local/go/bin:$PATH +export GOMAXPROCS=8 +systemctl show mithril-alpenglow-pr259.service -p MainPID -p ActiveState > "$review_dir/results/service-before.txt" +sha256sum /tmp/mithril-leader-packing-pr-20260913.tar.gz /tmp/mithril-leader-base-20260913.tar > "$review_dir/results/source-sha256.txt" +lscpu -e=CPU,CORE,SOCKET > "$review_dir/results/topology.txt" +cd "$review_dir/candidate" +nice -n 10 go test -race ./pkg/accounts ./pkg/blockprod/... ./pkg/costmodel ./pkg/merkletree ./pkg/replay ./pkg/fees ./pkg/turbine ./pkg/tpu/txfixture ./pkg/config ./cmd/mithril/node > "$review_dir/results/native-race.txt" 2>&1 +nice -n 10 go vet ./pkg/accounts ./pkg/blockprod/... ./pkg/costmodel ./pkg/merkletree ./pkg/replay ./pkg/fees ./pkg/turbine ./pkg/tpu/txfixture ./pkg/config ./cmd/mithril/node > "$review_dir/results/native-vet.txt" 2>&1 +nice -n 10 go build -o "$review_dir/results/mithril" ./cmd/mithril > "$review_dir/results/native-build.txt" 2>&1 +nice -n 10 go test -c -o "$review_dir/results/candidate.test" ./pkg/blockprod +cd "$review_dir/baseline" +nice -n 10 go test -run '^TestReadonlyPairBlockCapacityAndShredRoundTrip$' ./pkg/blockprod > "$review_dir/results/baseline-capacity.txt" 2>&1 +nice -n 10 go test -c -o "$review_dir/results/baseline.test" ./pkg/blockprod +for round in 1 2 3; do + if [ "$round" = 2 ]; then order='candidate baseline'; else order='baseline candidate'; fi + for variant in $order; do + nice -n 10 taskset -c 0-7 "$review_dir/results/$variant.test" -test.run='^$' -test.bench='^BenchmarkReadonlyPair(FullBlock|PreparedFullBlock)$' -test.benchtime=3x -test.count=1 > "$review_dir/results/round-$round-$variant.txt" 2>&1 + done +done +systemctl show mithril-alpenglow-pr259.service -p MainPID -p ActiveState > "$review_dir/results/service-after.txt" +sha256sum "$review_dir/results/"*.test "$review_dir/results/mithril" > "$review_dir/results/binary-sha256.txt" +cat "$review_dir/results/native-race.txt" +cat "$review_dir/results/round-"*.txt diff --git a/docs/results/leader-block-packing/2026-09-13/service-after.txt b/docs/results/leader-block-packing/2026-09-13/service-after.txt new file mode 100644 index 000000000..bece5e701 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/service-after.txt @@ -0,0 +1,2 @@ +ActiveState=active +MainPID=3846480 diff --git a/docs/results/leader-block-packing/2026-09-13/service-before.txt b/docs/results/leader-block-packing/2026-09-13/service-before.txt new file mode 100644 index 000000000..bece5e701 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/service-before.txt @@ -0,0 +1,2 @@ +ActiveState=active +MainPID=3846480 diff --git a/docs/results/leader-block-packing/2026-09-13/source-sha256.txt b/docs/results/leader-block-packing/2026-09-13/source-sha256.txt new file mode 100644 index 000000000..c4996c08f --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/source-sha256.txt @@ -0,0 +1,2 @@ +c08b4bee669cd754aed39abe4235ab493679ce15ac9f12f928027ea40015c830 /tmp/mithril-leader-packing-pr-20260913.tar.gz +e695e40e720139547b6b6ad5b84ed65ff1d21423874ef352a5bdb681121065ae /tmp/mithril-leader-base-20260913.tar diff --git a/docs/results/leader-block-packing/2026-09-13/summarize.py b/docs/results/leader-block-packing/2026-09-13/summarize.py new file mode 100644 index 000000000..6ae50531f --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/summarize.py @@ -0,0 +1,20 @@ +"""Summarize the paired standalone bank benchmarks; run beside the raw logs.""" +import json +from pathlib import Path +from statistics import median + +root = Path(__file__).resolve().parent +samples = {} +for path in sorted(root.glob('round-*.txt')): + variant = path.stem.rsplit('-', 1)[1] + for line in path.read_text().splitlines(): + if not line.startswith('Benchmark'): + continue + fields = line.split() + name = fields[0].rsplit('-', 1)[0] + metrics = dict(zip(fields[3::2], map(float, fields[2::2]))) + samples.setdefault(variant + ':' + name, []).append(metrics) +summary = {key: {metric: median(s[metric] for s in values) for metric in values[0]} + for key, values in sorted(samples.items())} +(root / 'summary.json').write_text(json.dumps({'samples': samples, 'medians': summary}, indent=2) + '\n') +print(json.dumps(summary, indent=2)) diff --git a/docs/results/leader-block-packing/2026-09-13/summary.json b/docs/results/leader-block-packing/2026-09-13/summary.json new file mode 100644 index 000000000..c8bcef79d --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/summary.json @@ -0,0 +1,136 @@ +{ + "samples": { + "baseline:BenchmarkReadonlyPairFullBlock/wire": [ + { + "ns/op": 209635460.0, + "tx/block": 48622.0, + "B/op": 259271528.0, + "allocs/op": 3363451.0 + }, + { + "ns/op": 206873746.0, + "tx/block": 48622.0, + "B/op": 259272293.0, + "allocs/op": 3363455.0 + }, + { + "ns/op": 209864927.0, + "tx/block": 48622.0, + "B/op": 259274824.0, + "allocs/op": 3363464.0 + } + ], + "baseline:BenchmarkReadonlyPairFullBlock/decoded": [ + { + "ns/op": 188551991.0, + "tx/block": 48622.0, + "B/op": 237879586.0, + "allocs/op": 3071729.0 + }, + { + "ns/op": 214317833.0, + "tx/block": 48622.0, + "B/op": 237880618.0, + "allocs/op": 3071736.0 + }, + { + "ns/op": 189729779.0, + "tx/block": 48622.0, + "B/op": 237880589.0, + "allocs/op": 3071738.0 + } + ], + "candidate:BenchmarkReadonlyPairFullBlock/wire": [ + { + "ns/op": 167507945.0, + "tx/block": 48622.0, + "B/op": 223454984.0, + "allocs/op": 2728489.0 + }, + { + "ns/op": 145202527.0, + "tx/block": 48622.0, + "B/op": 223454946.0, + "allocs/op": 2728489.0 + }, + { + "ns/op": 150295927.0, + "tx/block": 48622.0, + "B/op": 223455032.0, + "allocs/op": 2728489.0 + } + ], + "candidate:BenchmarkReadonlyPairFullBlock/decoded": [ + { + "ns/op": 129990183.0, + "tx/block": 48622.0, + "B/op": 202061266.0, + "allocs/op": 2436756.0 + }, + { + "ns/op": 151023757.0, + "tx/block": 48622.0, + "B/op": 202061384.0, + "allocs/op": 2436757.0 + }, + { + "ns/op": 132400481.0, + "tx/block": 48622.0, + "B/op": 202061368.0, + "allocs/op": 2436757.0 + } + ], + "candidate:BenchmarkReadonlyPairPreparedFullBlock": [ + { + "ns/op": 121710569.0, + "tx/block": 48622.0, + "B/op": 173666002.0, + "allocs/op": 1950536.0 + }, + { + "ns/op": 101921901.0, + "tx/block": 48622.0, + "B/op": 173666021.0, + "allocs/op": 1950536.0 + }, + { + "ns/op": 101595082.0, + "tx/block": 48622.0, + "B/op": 173666032.0, + "allocs/op": 1950536.0 + } + ] + }, + "medians": { + "baseline:BenchmarkReadonlyPairFullBlock/decoded": { + "ns/op": 189729779.0, + "tx/block": 48622.0, + "B/op": 237880589.0, + "allocs/op": 3071736.0 + }, + "baseline:BenchmarkReadonlyPairFullBlock/wire": { + "ns/op": 209635460.0, + "tx/block": 48622.0, + "B/op": 259272293.0, + "allocs/op": 3363455.0 + }, + "candidate:BenchmarkReadonlyPairFullBlock/decoded": { + "ns/op": 132400481.0, + "tx/block": 48622.0, + "B/op": 202061368.0, + "allocs/op": 2436757.0 + }, + "candidate:BenchmarkReadonlyPairFullBlock/wire": { + "ns/op": 150295927.0, + "tx/block": 48622.0, + "B/op": 223454984.0, + "allocs/op": 2728489.0 + }, + "candidate:BenchmarkReadonlyPairPreparedFullBlock": { + "ns/op": 101921901.0, + "tx/block": 48622.0, + "B/op": 173666021.0, + "allocs/op": 1950536.0 + } + } +} diff --git a/docs/results/leader-block-packing/2026-09-13/topology.txt b/docs/results/leader-block-packing/2026-09-13/topology.txt new file mode 100644 index 000000000..4647ea847 --- /dev/null +++ b/docs/results/leader-block-packing/2026-09-13/topology.txt @@ -0,0 +1,17 @@ +CPU CORE SOCKET + 0 0 0 + 1 1 0 + 2 2 0 + 3 3 0 + 4 4 0 + 5 5 0 + 6 6 0 + 7 7 0 + 8 0 0 + 9 1 0 + 10 2 0 + 11 3 0 + 12 4 0 + 13 5 0 + 14 6 0 + 15 7 0 From ea5a182be0b6c4c84b824050fa0943aeb63f63a6 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:20:11 -0500 Subject: [PATCH 3/9] Bound scheduler transaction references in both priority heaps --- pkg/blockprod/scheduler/buffer.go | 64 ++++--- pkg/blockprod/scheduler/buffer_order_test.go | 5 +- .../scheduler/buffer_retention_test.go | 158 ++++++++++++++++++ 3 files changed, 189 insertions(+), 38 deletions(-) create mode 100644 pkg/blockprod/scheduler/buffer_retention_test.go diff --git a/pkg/blockprod/scheduler/buffer.go b/pkg/blockprod/scheduler/buffer.go index bb493529c..38bb57a95 100644 --- a/pkg/blockprod/scheduler/buffer.go +++ b/pkg/blockprod/scheduler/buffer.go @@ -28,6 +28,9 @@ type entry struct { skipGen uint64 alive bool + // Indexes belong to Buffer.mu. Every buffered entry appears exactly once + // in each heap; -1 denotes absence while an entry is owned by the consumer. + maxIndex, minIndex int } type maxHeap []*entry @@ -38,15 +41,18 @@ func (h maxHeap) Less(i, j int) bool { } func (h maxHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] + h[i].maxIndex, h[j].maxIndex = i, j } func (h *maxHeap) Push(x any) { e := x.(*entry) + e.maxIndex = len(*h) *h = append(*h, e) } func (h *maxHeap) Pop() any { old := *h n := len(old) e := old[n-1] + e.maxIndex = -1 old[n-1] = nil *h = old[:n-1] return e @@ -64,15 +70,18 @@ func (h minHeap) Less(i, j int) bool { } func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] + h[i].minIndex, h[j].minIndex = i, j } func (h *minHeap) Push(x any) { e := x.(*entry) + e.minIndex = len(*h) *h = append(*h, e) } func (h *minHeap) Pop() any { old := *h n := len(old) e := old[n-1] + e.minIndex = -1 old[n-1] = nil *h = old[:n-1] return e @@ -162,21 +171,19 @@ func (b *Buffer) Cleanup(drop func(*entry) bool) int { b.mu.Lock() defer b.mu.Unlock() - var doomed []*entry + dropped := 0 for _, e := range b.byHash { - if e.alive && drop(e) { - doomed = append(doomed, e) + if drop(e) { + b.killLocked(e) + dropped++ } } - for _, e := range doomed { - b.killLocked(e) - } - b.drainDeadLocked() - return len(doomed) + return dropped } func (b *Buffer) pushAliveLocked(e *entry) { e.alive = true + e.maxIndex, e.minIndex = -1, -1 b.byHash[e.messageHash] = e heap.Push(&b.max, e) heap.Push(&b.min, e) @@ -187,17 +194,20 @@ func (b *Buffer) killLocked(e *entry) { if e == nil || !e.alive { return } + if e.maxIndex >= 0 { + heap.Remove(&b.max, e.maxIndex) + } + if e.minIndex >= 0 { + heap.Remove(&b.min, e.minIndex) + } e.alive = false delete(b.byHash, e.messageHash) b.alive-- } func (b *Buffer) popMaxAliveLocked() *entry { - for b.max.Len() > 0 { + if b.max.Len() > 0 { e := b.max.popEntry() - if !e.alive { - continue - } b.killLocked(e) return e } @@ -210,6 +220,7 @@ func (b *Buffer) popMaxAliveLocked() *entry { func (h *maxHeap) popEntry() *entry { nodes := *h root := nodes[0] + root.maxIndex = -1 last := nodes[len(nodes)-1] nodes[len(nodes)-1] = nil nodes = nodes[:len(nodes)-1] @@ -227,9 +238,11 @@ func (h *maxHeap) popEntry() *entry { break } nodes[i] = nodes[child] + nodes[i].maxIndex = i i = child } nodes[i] = last + last.maxIndex = i } *h = nodes return root @@ -243,38 +256,17 @@ func higherPriority(a, b *entry) bool { } func (b *Buffer) peekMinAliveLocked() *entry { - for b.min.Len() > 0 { - if b.min[0].alive { - return b.min[0] - } - heap.Pop(&b.min) + if b.min.Len() > 0 { + return b.min[0] } return nil } func (b *Buffer) popMinAliveLocked() *entry { - for b.min.Len() > 0 { + if b.min.Len() > 0 { e := heap.Pop(&b.min).(*entry) - if !e.alive { - continue - } b.killLocked(e) return e } return nil } - -func (b *Buffer) drainDeadLocked() { - for b.max.Len() > 0 { - if b.max[0].alive { - break - } - heap.Pop(&b.max) - } - for b.min.Len() > 0 { - if b.min[0].alive { - break - } - heap.Pop(&b.min) - } -} diff --git a/pkg/blockprod/scheduler/buffer_order_test.go b/pkg/blockprod/scheduler/buffer_order_test.go index 7ed342c33..11a091bfb 100644 --- a/pkg/blockprod/scheduler/buffer_order_test.go +++ b/pkg/blockprod/scheduler/buffer_order_test.go @@ -35,8 +35,8 @@ func TestMaxHeapRemovalMatchesSortedOrder(t *testing.T) { } // Compare interleaved insertion, eviction, cleanup and removal with a small -// unsorted reference model. Reusing hashes after removal also exercises stale -// nodes left in the other heap without reusing the entry objects themselves. +// unsorted reference model. Reusing hashes after removal exercises replacement +// entries as well as counterpart removal from both indexed heaps. func TestBufferMixedOperationsMatchReference(t *testing.T) { const capacity = 64 rng := rand.New(rand.NewSource(418)) @@ -95,6 +95,7 @@ func TestBufferMixedOperationsMatchReference(t *testing.T) { require.Equal(t, want, b.Cleanup(func(e *entry) bool { return e.seq%11 == mod })) } require.Equal(t, len(model), b.Len(), "step=%d", step) + assertBufferIndexes(t, b) } for len(model) > 0 { want := best(true) diff --git a/pkg/blockprod/scheduler/buffer_retention_test.go b/pkg/blockprod/scheduler/buffer_retention_test.go new file mode 100644 index 000000000..6b24aac8a --- /dev/null +++ b/pkg/blockprod/scheduler/buffer_retention_test.go @@ -0,0 +1,158 @@ +package scheduler + +import ( + "encoding/binary" + "sync" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func retainedTestEntry(id, reward uint64) *entry { + e := &entry{tx: &solana.Transaction{}, wire: []byte{1, 2, 3}, seq: id, reward: reward} + binary.LittleEndian.PutUint64(e.messageHash[:], id) + return e +} + +// Check both membership and backing-array references: shrinking a slice alone +// must not leave transaction payloads reachable outside its visible length. +func assertBufferIndexes(t *testing.T, b *Buffer) { + t.Helper() + b.mu.Lock() + defer b.mu.Unlock() + require.Equal(t, b.alive, len(b.byHash)) + require.Equal(t, b.alive, len(b.max)) + require.Equal(t, b.alive, len(b.min)) + require.LessOrEqual(t, b.alive, b.capacity) + for i, e := range b.max { + require.True(t, e.alive) + require.Equal(t, i, e.maxIndex) + require.Same(t, e, b.byHash[e.messageHash]) + require.Same(t, e, b.min[e.minIndex]) + if i > 0 { + require.False(t, b.max.Less(i, (i-1)/2)) + } + } + for i, e := range b.min { + require.Equal(t, i, e.minIndex) + require.Same(t, e, b.max[e.maxIndex]) + if i > 0 { + require.False(t, b.min.Less(i, (i-1)/2)) + } + } + for _, backing := range [][]*entry{b.max[:cap(b.max)], b.min[:cap(b.min)]} { + for _, e := range backing[b.alive:] { + require.Nil(t, e) + } + } +} + +func TestBufferConsumedEntriesReleaseBothHeapReferences(t *testing.T) { + b := NewBuffer(256) + pinned := retainedTestEntry(0, 1) + b.Insert(pinned) + for i := uint64(1); i <= 100000; i++ { + e := retainedTestEntry(i, 2) + result, _ := b.Insert(e) + require.Equal(t, InsertAccepted, result) + require.Same(t, e, b.PopMax()) + // The consumer still owns usable payloads after index removal. + require.NotNil(t, e.tx) + require.Equal(t, []byte{1, 2, 3}, e.wire) + if i%1000 == 0 { + assertBufferIndexes(t, b) + } + } + b.Cleanup(func(*entry) bool { return false }) + assertBufferIndexes(t, b) + t.Logf("capacity=%d active=%d max_refs=%d min_refs=%d", b.capacity, b.Len(), len(b.max), len(b.min)) + require.Same(t, pinned, b.PopMax()) + assertBufferIndexes(t, b) +} + +func TestBufferEvictionAndCleanupReleaseBothHeapReferences(t *testing.T) { + b := NewBuffer(2) + pinned := retainedTestEntry(0, 1000000) + b.Insert(pinned) + previous := retainedTestEntry(1, 1) + b.Insert(previous) + for i := uint64(2); i < 10000; i++ { + next := retainedTestEntry(i, i) + result, evicted := b.Insert(next) + require.Equal(t, InsertAccepted, result) + require.Same(t, previous, evicted) + require.False(t, evicted.alive) + require.Equal(t, -1, evicted.maxIndex) + require.Equal(t, -1, evicted.minIndex) + previous = next + if i%100 == 0 { + assertBufferIndexes(t, b) + } + } + require.Equal(t, 1, b.Cleanup(func(e *entry) bool { return e != pinned })) + assertBufferIndexes(t, b) + require.Same(t, pinned, b.PopMax()) + assertBufferIndexes(t, b) +} + +func TestBufferRepeatedRebufferPreservesNewHigherPriorityArrivals(t *testing.T) { + s := New(nil) + s.bankGen = 1 + skipped := retainedTestEntry(1, 10) + skipped.skipGen = s.bankGen + s.buffer.Insert(skipped) + for i := uint64(2); i < 10002; i++ { + low := retainedTestEntry(i, 1) + s.buffer.Insert(low) + picked, retry := s.popSchedulable(s.bankGen) + require.Same(t, low, picked) + require.Equal(t, []*entry{skipped}, retry) + for _, e := range retry { + s.rebuffer(e) + } + if i%100 == 0 { + assertBufferIndexes(t, s.buffer) + } + } + // Preserve the existing scan/retry policy when a higher-fee packet arrives. + high := retainedTestEntry(20000, 20) + s.buffer.Insert(high) + picked, retry := s.popSchedulable(s.bankGen) + require.Same(t, high, picked) + require.Empty(t, retry) + // A later bank may retry the previously skipped transaction. + s.bankGen++ + picked, retry = s.popSchedulable(s.bankGen) + require.Same(t, skipped, picked) + require.Empty(t, retry) + assertBufferIndexes(t, s.buffer) +} + +func TestBufferConcurrentInsertRemovalAndCleanup(t *testing.T) { + b := NewBuffer(64) + var workers sync.WaitGroup + for worker := uint64(0); worker < 4; worker++ { + workers.Go(func() { + for i := uint64(0); i < 2000; i++ { + id := worker*2000 + i + b.Insert(retainedTestEntry(id, id%17)) + } + }) + } + workers.Go(func() { + for i := 0; i < 8000; i++ { + b.PopMax() + } + }) + workers.Go(func() { + for i := 0; i < 100; i++ { + b.Cleanup(func(e *entry) bool { return e.seq%3 == 0 }) + } + }) + workers.Wait() + assertBufferIndexes(t, b) + for b.PopMax() != nil { + } + assertBufferIndexes(t, b) +} From 185673dc2b35f4c7c6adc9ea925331b04027e3e5 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:40:31 -0500 Subject: [PATCH 4/9] Document scheduler retention tradeoffs and near-limit block results --- docs/leader_block_packing.md | 8 +- .../queue-retention/2026-09-14/README.md | 79 ++++++++++++++++++ .../2026-09-14/ci-race-local.log | 8 ++ .../2026-09-14/drain-1-baseline.log | 7 ++ .../2026-09-14/drain-1-candidate.log | 7 ++ .../2026-09-14/drain-2-baseline.log | 7 ++ .../2026-09-14/drain-2-candidate.log | 7 ++ .../2026-09-14/drain-3-baseline.log | 7 ++ .../2026-09-14/drain-3-candidate.log | 7 ++ .../2026-09-14/drain-runs.json | 80 +++++++++++++++++++ .../2026-09-14/drain-summary.json | 38 +++++++++ .../2026-09-14/queue-fixed-race.log | 1 + .../queue-retention/2026-09-14/retention.log | 4 + 13 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 docs/results/queue-retention/2026-09-14/README.md create mode 100644 docs/results/queue-retention/2026-09-14/ci-race-local.log create mode 100644 docs/results/queue-retention/2026-09-14/drain-1-baseline.log create mode 100644 docs/results/queue-retention/2026-09-14/drain-1-candidate.log create mode 100644 docs/results/queue-retention/2026-09-14/drain-2-baseline.log create mode 100644 docs/results/queue-retention/2026-09-14/drain-2-candidate.log create mode 100644 docs/results/queue-retention/2026-09-14/drain-3-baseline.log create mode 100644 docs/results/queue-retention/2026-09-14/drain-3-candidate.log create mode 100644 docs/results/queue-retention/2026-09-14/drain-runs.json create mode 100644 docs/results/queue-retention/2026-09-14/drain-summary.json create mode 100644 docs/results/queue-retention/2026-09-14/queue-fixed-race.log create mode 100644 docs/results/queue-retention/2026-09-14/retention.log diff --git a/docs/leader_block_packing.md b/docs/leader_block_packing.md index c650221e7..332c1a813 100644 --- a/docs/leader_block_packing.md +++ b/docs/leader_block_packing.md @@ -10,8 +10,12 @@ execution and all resource budgets remain bank-dependent checks. Leader execution reuses borrowed-account scratch and skips detailed replay stage timers. Missing-current-bank lookup errors defer base58 formatting until used, avoiding wasted work before parent lookup. Entry Merkle hashing retains -only the root-building scratch, and queue removal avoids heap interface dispatch -and unused index updates while preserving priority/FIFO order. +only the root-building scratch, and max-heap removal avoids heap interface dispatch +while preserving priority/FIFO order. Both priority heaps now track entry +indexes so consumption, eviction and expiry remove every buffer reference. +Repeated rebuffering reuses the caller's intact transaction without accumulating +duplicate heap references. The existing slot-local skip scanning policy remains +unchanged, including selection of newly arrived higher-priority transactions. ## Capacity and protocol limits diff --git a/docs/results/queue-retention/2026-09-14/README.md b/docs/results/queue-retention/2026-09-14/README.md new file mode 100644 index 000000000..ef631bbd8 --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/README.md @@ -0,0 +1,79 @@ +# Bounded scheduler references and regression CI + +Follow-up to PR 279 at `b24de8e616b28eccd1f86c3384a1af96bf9624c0`, with the +queue fix in `667d002f`. This bounds scheduler references and adds regression +CI. The existing slot-local skip scanning/rebuffering policy is unchanged; +`scheduler.go` is identical to the comparison commit. + +## Queue lifetime + +Each entry now records its position in both priority heaps. Consumption, +eviction and expiration remove its counterpart directly, clear the removed +backing-array pointer and release the deduplication-map reference. Returned +transactions retain their owned wire, decoded and prepared data for the caller; +rebuffering inserts exactly one reference in each heap again. Neither priority +ordering, equal-reward FIFO, capacity, fee scoring nor cross-slot retry rules +change. + +The regression keeps one low-reward entry while inserting and consuming 100,000 +higher-reward entries. With capacity 256, the old algorithm retained 100,001 +min-heap references. The candidate finishes with **one active entry and one +reference in each heap**, without requiring a later cleanup sweep. Additional +tests cover symmetric eviction, expiration, repeated rebuffering, new +higher-priority arrivals, retry in a later bank, concurrent operations, exact +reference-model ordering and zeroed slice storage beyond the visible length. + +Both heaps' lengths and map membership equal the number of buffered entries. +Backing-array allocation can retain its high-water capacity, but unused positions +are nil and cannot retain transaction payloads. + +## Validation + +Apple M4 Pro, Go 1.26.4. The scheduler race suite passed with GOMAXPROCS=4. +Scheduler vet and a complete `./cmd/mithril` build also passed. +The exact new CI command also passed locally with GOMAXPROCS=2: + +```sh +GOMAXPROCS=2 go test -race -p 2 -count=1 \ + ./pkg/alpenglow ./pkg/consensus ./pkg/replay \ + ./pkg/turbine ./pkg/sigverify ./pkg/blockprod/... ./cmd/mithril/node +``` + +The new `regression-tests` GitHub job runs these complete package suites, +including reservation/crash recovery, background history ordering, checkpoint +capture and streaming cancellation, alongside the existing build job. This is +not the full repository suite; pre-existing sealevel failures remain documented +in the [combined validation report](../../validator-performance/2026-09-14/README.md). + +Logs: [scheduler race](queue-fixed-race.log), [CI command locally](ci-race-local.log), +[retention result](retention.log). + +## Removal-cost tradeoff + +Three alternating paired rounds of the existing `BenchmarkBufferDrain`, +GOMAXPROCS=1, each selecting all 120,000 transactions from a prefilled queue. +Queue construction and transaction preparation are excluded. Baseline uses the +PR's `b24de8e6` buffer; candidate uses indexed counterpart removal. Both run the +same benchmark. Timing excludes GC costs outside the timed region and does not +measure a sustained validator workload. + +| Priority distribution | Baseline median | Candidate median | +| --- | ---: | ---: | +| Equal rewards / FIFO | 143.0 ns/pop | 188.0 ns/pop | +| Mixed rewards | 254.0 ns/pop | 274.2 ns/pop | + +Both report zero timed allocations per pop. Equal-reward candidate samples were +188.0 / 302.4 / 183.5 ns, so the raw variability should not be hidden behind the +median. These measurements show the direct removal cost, not a net validator +speedup. Memory retention is bounded; the effect on block fullness, garbage +collection and FAST omissions remains unmeasured. No live deployment or +validator restart was performed for this follow-up. + +```sh +GOMAXPROCS=1 go test ./pkg/blockprod/scheduler -run '^$' \ + -bench '^BenchmarkBufferDrain$' -benchtime=120000x -count=3 +``` + +[All sample values](drain-summary.json), [execution order](drain-runs.json), +raw baseline/candidate logs alongside this report. These results are local M4 +measurements and must not be substituted for the earlier Zen 5 bank benchmarks. diff --git a/docs/results/queue-retention/2026-09-14/ci-race-local.log b/docs/results/queue-retention/2026-09-14/ci-race-local.log new file mode 100644 index 000000000..7fd86596a --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/ci-race-local.log @@ -0,0 +1,8 @@ +ok github.com/Overclock-Validator/mithril/pkg/alpenglow 7.643s +ok github.com/Overclock-Validator/mithril/pkg/consensus 9.953s +ok github.com/Overclock-Validator/mithril/pkg/replay 4.578s +ok github.com/Overclock-Validator/mithril/pkg/turbine 9.324s +ok github.com/Overclock-Validator/mithril/pkg/sigverify 23.233s +ok github.com/Overclock-Validator/mithril/pkg/blockprod 21.894s +ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 5.706s +ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.918s diff --git a/docs/results/queue-retention/2026-09-14/drain-1-baseline.log b/docs/results/queue-retention/2026-09-14/drain-1-baseline.log new file mode 100644 index 000000000..c525bd0d7 --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-1-baseline.log @@ -0,0 +1,7 @@ +goos: darwin +goarch: arm64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler +cpu: Apple M4 Pro +BenchmarkBufferDrain/equal_rewards 120000 143.0 ns/op 0 B/op 0 allocs/op +BenchmarkBufferDrain/mixed_rewards 120000 240.2 ns/op 0 B/op 0 allocs/op +PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-1-candidate.log b/docs/results/queue-retention/2026-09-14/drain-1-candidate.log new file mode 100644 index 000000000..426870829 --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-1-candidate.log @@ -0,0 +1,7 @@ +goos: darwin +goarch: arm64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler +cpu: Apple M4 Pro +BenchmarkBufferDrain/equal_rewards 120000 188.0 ns/op 0 B/op 0 allocs/op +BenchmarkBufferDrain/mixed_rewards 120000 268.0 ns/op 0 B/op 0 allocs/op +PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-2-baseline.log b/docs/results/queue-retention/2026-09-14/drain-2-baseline.log new file mode 100644 index 000000000..42fe15e61 --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-2-baseline.log @@ -0,0 +1,7 @@ +goos: darwin +goarch: arm64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler +cpu: Apple M4 Pro +BenchmarkBufferDrain/equal_rewards 120000 147.6 ns/op 0 B/op 0 allocs/op +BenchmarkBufferDrain/mixed_rewards 120000 261.2 ns/op 0 B/op 0 allocs/op +PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-2-candidate.log b/docs/results/queue-retention/2026-09-14/drain-2-candidate.log new file mode 100644 index 000000000..3aeae2d96 --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-2-candidate.log @@ -0,0 +1,7 @@ +goos: darwin +goarch: arm64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler +cpu: Apple M4 Pro +BenchmarkBufferDrain/equal_rewards 120000 302.4 ns/op 0 B/op 0 allocs/op +BenchmarkBufferDrain/mixed_rewards 120000 276.0 ns/op 0 B/op 0 allocs/op +PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-3-baseline.log b/docs/results/queue-retention/2026-09-14/drain-3-baseline.log new file mode 100644 index 000000000..6a332b50f --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-3-baseline.log @@ -0,0 +1,7 @@ +goos: darwin +goarch: arm64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler +cpu: Apple M4 Pro +BenchmarkBufferDrain/equal_rewards 120000 140.1 ns/op 0 B/op 0 allocs/op +BenchmarkBufferDrain/mixed_rewards 120000 254.0 ns/op 0 B/op 0 allocs/op +PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-3-candidate.log b/docs/results/queue-retention/2026-09-14/drain-3-candidate.log new file mode 100644 index 000000000..b79128a8e --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-3-candidate.log @@ -0,0 +1,7 @@ +goos: darwin +goarch: arm64 +pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler +cpu: Apple M4 Pro +BenchmarkBufferDrain/equal_rewards 120000 183.5 ns/op 0 B/op 0 allocs/op +BenchmarkBufferDrain/mixed_rewards 120000 274.2 ns/op 0 B/op 0 allocs/op +PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-runs.json b/docs/results/queue-retention/2026-09-14/drain-runs.json new file mode 100644 index 000000000..df2c7f65c --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-runs.json @@ -0,0 +1,80 @@ +[ + { + "round": 1, + "variant": "baseline", + "command": [ + "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/baseline-scheduler.test", + "-test.run=^$", + "-test.bench=^BenchmarkBufferDrain$", + "-test.benchtime=120000x", + "-test.count=1" + ], + "exit": 0, + "seconds": 0.6474292278289795 + }, + { + "round": 1, + "variant": "candidate", + "command": [ + "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/candidate-scheduler.test", + "-test.run=^$", + "-test.bench=^BenchmarkBufferDrain$", + "-test.benchtime=120000x", + "-test.count=1" + ], + "exit": 0, + "seconds": 0.6436319351196289 + }, + { + "round": 2, + "variant": "candidate", + "command": [ + "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/candidate-scheduler.test", + "-test.run=^$", + "-test.bench=^BenchmarkBufferDrain$", + "-test.benchtime=120000x", + "-test.count=1" + ], + "exit": 0, + "seconds": 0.306088924407959 + }, + { + "round": 2, + "variant": "baseline", + "command": [ + "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/baseline-scheduler.test", + "-test.run=^$", + "-test.bench=^BenchmarkBufferDrain$", + "-test.benchtime=120000x", + "-test.count=1" + ], + "exit": 0, + "seconds": 0.2681159973144531 + }, + { + "round": 3, + "variant": "baseline", + "command": [ + "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/baseline-scheduler.test", + "-test.run=^$", + "-test.bench=^BenchmarkBufferDrain$", + "-test.benchtime=120000x", + "-test.count=1" + ], + "exit": 0, + "seconds": 0.2570068836212158 + }, + { + "round": 3, + "variant": "candidate", + "command": [ + "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/candidate-scheduler.test", + "-test.run=^$", + "-test.bench=^BenchmarkBufferDrain$", + "-test.benchtime=120000x", + "-test.count=1" + ], + "exit": 0, + "seconds": 0.269057035446167 + } +] diff --git a/docs/results/queue-retention/2026-09-14/drain-summary.json b/docs/results/queue-retention/2026-09-14/drain-summary.json new file mode 100644 index 000000000..217c079f2 --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/drain-summary.json @@ -0,0 +1,38 @@ +{ + "samples_ns_per_pop": { + "baseline": { + "equal_rewards": [ + 143.0, + 147.6, + 140.1 + ], + "mixed_rewards": [ + 240.2, + 261.2, + 254.0 + ] + }, + "candidate": { + "equal_rewards": [ + 188.0, + 302.4, + 183.5 + ], + "mixed_rewards": [ + 268.0, + 276.0, + 274.2 + ] + } + }, + "median_ns_per_pop": { + "baseline": { + "equal_rewards": 143.0, + "mixed_rewards": 254.0 + }, + "candidate": { + "equal_rewards": 188.0, + "mixed_rewards": 274.2 + } + } +} diff --git a/docs/results/queue-retention/2026-09-14/queue-fixed-race.log b/docs/results/queue-retention/2026-09-14/queue-fixed-race.log new file mode 100644 index 000000000..5241695b8 --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/queue-fixed-race.log @@ -0,0 +1 @@ +ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 5.106s diff --git a/docs/results/queue-retention/2026-09-14/retention.log b/docs/results/queue-retention/2026-09-14/retention.log new file mode 100644 index 000000000..86627651b --- /dev/null +++ b/docs/results/queue-retention/2026-09-14/retention.log @@ -0,0 +1,4 @@ +=== RUN TestBufferConsumedEntriesReleaseBothHeapReferences + buffer_retention_test.go:69: capacity=256 active=1 max_refs=1 min_refs=1 +--- PASS: TestBufferConsumedEntriesReleaseBothHeapReferences (0.10s) +PASS From cfb00cdbfba65b49e4510245de83354ed4061317 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:45:28 -0500 Subject: [PATCH 5/9] Document isolated PR validation and retain relevant benchmark evidence --- docs/results/pr-split-2026-09-15/leader/README.md | 7 +++++++ .../pr-split-2026-09-15/leader/leader-tests.log | 10 ++++++++++ docs/results/pr-split-2026-09-15/leader/leader-vet.log | 0 3 files changed, 17 insertions(+) create mode 100644 docs/results/pr-split-2026-09-15/leader/README.md create mode 100644 docs/results/pr-split-2026-09-15/leader/leader-tests.log create mode 100644 docs/results/pr-split-2026-09-15/leader/leader-vet.log diff --git a/docs/results/pr-split-2026-09-15/leader/README.md b/docs/results/pr-split-2026-09-15/leader/README.md new file mode 100644 index 000000000..6fe67c2e5 --- /dev/null +++ b/docs/results/pr-split-2026-09-15/leader/README.md @@ -0,0 +1,7 @@ +# Review branch validation, September 15 + +This PR is split from #279 plus the later working-tree improvements. The tested code commit before this documentation commit was `c10fa0d871caa866c14a85892f8d5947358eb414`. Tests ran locally on Apple M4 Pro, Go1.26.4, GOMAXPROCS3 and package parallelism2. Logs beside this file are the fresh split-branch checks, not native measurements. Existing Zen5 benchmark documents retain their original baselines and scope. No validator restart or deployment occurred during this reorganization. + +Four independent branches start at current alpenglow-dev33dde405. Voting is based on the certificate-processing PR; leader packing is based on the streaming-preparation PR. Runtime changes and status-cache changes are independent. Shared CLI/configuration additions need an ordinary three-file merge reconciliation when combining leader packing and voting. A separate audit checkout reconciled these additions and matched the preserved full implementation exactly across Go sources, module files, TOML configuration and CI. + +The branch-specific race suites and vet passed. The combined audit has a separately documented pre-existing intermittent peer reconnect timeout; this is not reported as an entirely green combined race run. diff --git a/docs/results/pr-split-2026-09-15/leader/leader-tests.log b/docs/results/pr-split-2026-09-15/leader/leader-tests.log new file mode 100644 index 000000000..639c0e7a4 --- /dev/null +++ b/docs/results/pr-split-2026-09-15/leader/leader-tests.log @@ -0,0 +1,10 @@ +ok github.com/Overclock-Validator/mithril/pkg/blockprod 21.424s +ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 5.248s +ok github.com/Overclock-Validator/mithril/pkg/accounts 1.281s +ok github.com/Overclock-Validator/mithril/pkg/config 1.157s +ok github.com/Overclock-Validator/mithril/pkg/costmodel 1.512s +ok github.com/Overclock-Validator/mithril/pkg/merkletree 1.452s +ok github.com/Overclock-Validator/mithril/pkg/replay 4.406s +ok github.com/Overclock-Validator/mithril/pkg/tpu/txfixture 53.711s +ok github.com/Overclock-Validator/mithril/pkg/turbine 9.829s +ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.892s diff --git a/docs/results/pr-split-2026-09-15/leader/leader-vet.log b/docs/results/pr-split-2026-09-15/leader/leader-vet.log new file mode 100644 index 000000000..e69de29bb From a22ccd561704f4c07021dfdd94b3f84c9ec98f82 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:33:09 -0500 Subject: [PATCH 6/9] docs: keep review specifications and archive operational notes --- docs/leader-packing-evidence.md | 14 + docs/leader_block_packing.md | 2 +- .../leader-block-packing/2026-09-13/README.md | 129 ----- .../2026-09-13/baseline-capacity.txt | 1 - .../best-block/all-finalized-costs.json | 56 -- .../best-block/largest-block-metrics.json | 16 - .../2026-09-13/best-block/metadata.json | 17 - .../2026-09-13/best-block/send.json | 12 - .../2026-09-13/best-block/validation.json | 99 ---- .../2026-09-13/binary-sha256.txt | 3 - .../2026-09-13/evidence-manifest.json | 52 -- .../all-finalized-costs.json | 35 -- .../largest-block-metrics.json | 16 - .../four-block-prefill/metadata.json | 17 - .../2026-09-13/four-block-prefill/send.json | 11 - .../four-block-prefill/validation.json | 60 --- .../2026-09-13/live-preload.go.txt | 510 ------------------ .../2026-09-13/local-race.txt | 11 - .../2026-09-13/local-unit.txt | 10 - .../2026-09-13/native-build.txt | 0 .../2026-09-13/native-race.txt | 11 - .../2026-09-13/native-vet.txt | 0 .../2026-09-13/round-1-baseline.txt | 7 - .../2026-09-13/round-1-candidate.txt | 8 - .../2026-09-13/round-2-baseline.txt | 7 - .../2026-09-13/round-2-candidate.txt | 8 - .../2026-09-13/round-3-baseline.txt | 7 - .../2026-09-13/round-3-candidate.txt | 8 - .../2026-09-13/run-native.sh | 31 -- .../2026-09-13/service-after.txt | 2 - .../2026-09-13/service-before.txt | 2 - .../2026-09-13/source-sha256.txt | 2 - .../2026-09-13/summarize.py | 20 - .../2026-09-13/summary.json | 136 ----- .../2026-09-13/topology.txt | 17 - .../pr-split-2026-09-15/leader/README.md | 7 - .../leader/leader-tests.log | 10 - .../pr-split-2026-09-15/leader/leader-vet.log | 0 .../queue-retention/2026-09-14/README.md | 79 --- .../2026-09-14/ci-race-local.log | 8 - .../2026-09-14/drain-1-baseline.log | 7 - .../2026-09-14/drain-1-candidate.log | 7 - .../2026-09-14/drain-2-baseline.log | 7 - .../2026-09-14/drain-2-candidate.log | 7 - .../2026-09-14/drain-3-baseline.log | 7 - .../2026-09-14/drain-3-candidate.log | 7 - .../2026-09-14/drain-runs.json | 80 --- .../2026-09-14/drain-summary.json | 38 -- .../2026-09-14/queue-fixed-race.log | 1 - .../queue-retention/2026-09-14/retention.log | 4 - 50 files changed, 15 insertions(+), 1591 deletions(-) create mode 100644 docs/leader-packing-evidence.md delete mode 100644 docs/results/leader-block-packing/2026-09-13/README.md delete mode 100644 docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/metadata.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/send.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/best-block/validation.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/binary-sha256.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/evidence-manifest.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/live-preload.go.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/local-race.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/local-unit.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/native-build.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/native-race.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/native-vet.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/run-native.sh delete mode 100644 docs/results/leader-block-packing/2026-09-13/service-after.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/service-before.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/source-sha256.txt delete mode 100644 docs/results/leader-block-packing/2026-09-13/summarize.py delete mode 100644 docs/results/leader-block-packing/2026-09-13/summary.json delete mode 100644 docs/results/leader-block-packing/2026-09-13/topology.txt delete mode 100644 docs/results/pr-split-2026-09-15/leader/README.md delete mode 100644 docs/results/pr-split-2026-09-15/leader/leader-tests.log delete mode 100644 docs/results/pr-split-2026-09-15/leader/leader-vet.log delete mode 100644 docs/results/queue-retention/2026-09-14/README.md delete mode 100644 docs/results/queue-retention/2026-09-14/ci-race-local.log delete mode 100644 docs/results/queue-retention/2026-09-14/drain-1-baseline.log delete mode 100644 docs/results/queue-retention/2026-09-14/drain-1-candidate.log delete mode 100644 docs/results/queue-retention/2026-09-14/drain-2-baseline.log delete mode 100644 docs/results/queue-retention/2026-09-14/drain-2-candidate.log delete mode 100644 docs/results/queue-retention/2026-09-14/drain-3-baseline.log delete mode 100644 docs/results/queue-retention/2026-09-14/drain-3-candidate.log delete mode 100644 docs/results/queue-retention/2026-09-14/drain-runs.json delete mode 100644 docs/results/queue-retention/2026-09-14/drain-summary.json delete mode 100644 docs/results/queue-retention/2026-09-14/queue-fixed-race.log delete mode 100644 docs/results/queue-retention/2026-09-14/retention.log diff --git a/docs/leader-packing-evidence.md b/docs/leader-packing-evidence.md new file mode 100644 index 000000000..58be6cac7 --- /dev/null +++ b/docs/leader-packing-evidence.md @@ -0,0 +1,14 @@ +# Leader Packing: benchmark evidence + +The maintained subsystem documentation and reusable Go benchmarks describe the +implementation and reproduction method. Historical raw results and session +notes are retained at [the tested source snapshot](https://github.com/Overclock-Validator/mithril/blob/06ef067798c99947e8cc527450ad28430a9a7333) +(tag `review-evidence-20260916-leader-packing`). They are omitted from this proposed merge. + +[Historical result files](https://github.com/Overclock-Validator/mithril/tree/06ef067798c99947e8cc527450ad28430a9a7333/docs/results) + +Measurements retain their original baselines. Rebasing onto PR #278 does not +turn an intermediate-version benchmark into a comparison with the new base. +Component timings and short live observations do not establish sustained FAST +inclusion gains. The final review description records validation of the rebased +source separately from historical benchmark results. diff --git a/docs/leader_block_packing.md b/docs/leader_block_packing.md index 332c1a813..fd76fe466 100644 --- a/docs/leader_block_packing.md +++ b/docs/leader_block_packing.md @@ -92,7 +92,7 @@ work. Preloading 200k into a larger queue improved the observed four-block total while the first block still had less usable time and later banks awaited local replay/adoption. These observations do not isolate a single CPU bottleneck. -[Measured results, exact baselines and evidence](results/leader-block-packing/2026-09-13/README.md) +[Measured results, exact baselines and evidence](https://github.com/Overclock-Validator/mithril/blob/06ef067798c99947e8cc527450ad28430a9a7333/docs/results/leader-block-packing/2026-09-13/README.md) include both the successful near-limit block and the still-underfilled four-slot window. The archived live helper is historical experiment source with explicit cluster/identity/path constants; the offline fixture is the portable reproduction. diff --git a/docs/results/leader-block-packing/2026-09-13/README.md b/docs/results/leader-block-packing/2026-09-13/README.md deleted file mode 100644 index 06e270cb6..000000000 --- a/docs/results/leader-block-packing/2026-09-13/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# Leader block packing: standalone PR and live experiment results - -Goal: keep Mithril's own 200ms leader slots supplied and reduce work in the -packing window, while enforcing the actual cluster cost/byte limits. This PR -extracts the live-tested leader changes onto current `alpenglow-dev` at -`33dde4050d9250557583395810799aaac2f54017`. It can be reviewed independently of -FEC PR #259 and the streaming transaction-verification follow-up. - -## Fresh comparison against current alpenglow-dev - -Ryzen 7 9700X (8 physical cores/16 threads), Go 1.26.4, GOMAXPROCS=8, affinity -CPUs 0–7, nice 10. One serial benchmark caller builds a bank containing **48,622 -unique 198-byte one-signature, zero-instruction transactions**. Both versions -use identical in-memory accounts, 50M block-cost/20M writable-account budgets, -10MiB entry-byte limits and the same fixture/benchmark source. The copied test -harness is the only addition to the baseline; production code is unchanged. -Three paired rounds alternate order (base/PR, PR/base, base/PR); each reported -sample times three full banks. Tables show medians of the three sample means. -The validator continued running, so normal shared-host variation remains. - -| Bank input path | alpenglow-dev | PR | Allocated MB/block, base → PR | -|---|---:|---:|---:| -| Wire bytes | 209.64 ms | **150.30 ms** | 259.27 → 223.45 | -| Already decoded | 189.73 ms | **132.40 ms** | 237.88 → 202.06 | -| Already decoded and statically prepared | — | **101.92 ms** | — → 173.67 | - -The like-for-like wire path takes **28.3% less bank time**; the decoded path -30.2% less. The prepared bank phase additionally moves static work before -leadership. It does not eliminate that work from total CPU use. Its three -samples were 121.71, 101.92 and 101.60 ms. No whole-validator 2× claim is made. - -Timing includes bank admission, execution, account publication, entry building, -Merkle hashing and final entry flush. It excludes signing, signature verification, -bank setup, actual AccountsDB, network/shred broadcast, consensus and replay -adoption. Thus this is not a promise to fit the same block inside every live -packing window. The offline correctness test independently round-trips all -emitted entries through shred generation and decoding, verifies fees/hash/order, -and rejects the next transaction at the block-cost boundary. - -`run-native.sh` records the commands and baseline harness copying. Source and -binary hashes, topology, race/vet/build logs and every paired measurement are -included. `python3 summarize.py` rebuilds summary.json. The source archive was -captured before documentation-only additions; its Go/module files match the -published implementation. The live validator's PID stayed unchanged. - -## What enabled the live result - -- Decode owned TPU bytes once. Prepare immutable static transaction data before - leadership; recheck feature compatibility and bank-dependent state on use. -- Reuse borrowed-account scratch during serialized bank execution, and omit - detailed replay timing from that leader hot path. -- Format missing-bank-account diagnostics lazily before falling back to parent - accounts; calculate writable keys once and avoid base58 loader comparisons. -- Calculate signature Merkle roots without retaining unused proof nodes. -- Remove scheduler heap dispatch/index-store overhead, preserving priority/FIFO. -- Expose bounded queue capacity and local completion reserve as tuning settings, - keeping the 131,072-entry and 75ms defaults. -- Correct slot-time-dependent resource budgets. In the tested 200ms regime, - RaiseBlockLimitsTo100m means **50M**, not 100M per block. The skipped initial - 42,827-transaction compute-budget block exceeded the real limit and is not a - successful result. After correction, 42,338 such transactions finalized at - 49,916,502 cost units. The lighter readonly-pair workload then approached 48.6k. - -Slot-budget reference: [Agave release 2e10d67 slot parameters](https://github.com/anza-xyz/agave/blob/2e10d67f909c23a9586e1b3d099f7b8ef810ccfe/runtime/src/slot_params.rs), -including following-epoch activation and scaling of both account and block costs. - -## Live results: separate from the standalone benchmark - -The live binaries combined these changes with FEC/producer and streaming -verification work from trial `15ad1845`. No new live traffic or deployment was -performed while preparing this PR. These sequential windows are observations, -not controlled attribution of each optimization. - -**Best single finalized block:** slot **3073657**, **48,523 transactions**, one -signature each, zero instructions/execution CU, zero failures, **49,881,644 / -50,000,000 block-cost units**. Its local completion deadline margin was 49ms. -RPC blockhash: `H6iU43SrXp94LRvfpvhLDskboUJA4CupsEVFJcCRLANF`. - -The cost-only ceiling is floor(50,000,000 / 1,028) = 48,638. Upfront loaded-data -reservation reduces the offline admission limit to 48,622; runtime timing and -background work can reduce live inclusion further. The best live block is 99 -transactions below that offline admission count. We did not reach 50,000. - -The later fully preloaded four-block test offered 200,000 transactions at 150k -transactions/sec before our leader window, with queue capacity 262,144 and a -60ms completion reserve. The four finalized blocks were: - -| Slot | Included transactions | Cost units | Completion margin | -|---|---:|---:|---:| -| 3077076 | 26,687 | 27,434,236 | 39ms | -| 3077077 | 42,150 | 43,330,200 | 45ms | -| 3077078 | 41,387 | 42,545,836 | 41ms | -| 3077079 | 42,607 | 43,799,996 | 41ms | - -Total **152,831**, all successful; the other 47,169 expired without inclusion. -The preceding timely-refill run included 124,988. The 22.3% observed increase -coincides with different queue size, prefill timing, process age and live -conditions; it is not an isolated software speedup. **Four maximum-size blocks -were not achieved.** First-block packing time was shorter; later starts waited -for local replay/adoption. A reserve reduction does not add an equal amount of -packing time to every slot. Buffered log output was too delayed to be a useful -refill trigger, so the subsequent test used RPC slot observations and finally -full prefill. - -Small extracted finalized-validation records are in best-block/ and -four-block-prefill/. evidence-manifest.json hashes the original source records. -Full prior evidence archives remain identified by SHA-256: - -- Best-block experiment: `f785aeaad00d5cc85e868bf1488e4ebb7adb0346f97308f0fc6d8ae538c943a5`. -- Four-block prefill: `e7a3f03b5f88f4110c9dc58d306f14142bee4c5ba2f33b5b348d5ea05ded525f`. - -`live-preload.go.txt` is the original bounded live sender source (no key material). -Its RPC/genesis/identity/path constants are historical experiment settings, not -portable defaults. The offline tests reproduce its message construction and -capacity checks without funds or network. See [reproduction commands](../../../leader_block_packing.md). - -## Validation and remaining work - -Local and native Zen 5 race suites passed for accounts, blockprod/scheduler, -costmodel, merkletree, replay, fees, turbine, txfixture, config and node. Vet and -full Mithril builds passed on both machines. The full repository suite is not -claimed to pass: prior baseline/candidate trials reproduced unrelated BPF-loader -failures. Tests cover owned packet lifetimes, stale feature preparation, -current payer-state changes, fees-only/instruction failures, duplicates/age, -arena reuse, queue ordering/eviction, Merkle parity and slot-budget activation. - -The discarded status-map allocation experiment showed no clear timing gain and -is not included. Reusing message hashes through local replay and shortening -replay/adoption transitions remain possible follow-ups, not completed gains. diff --git a/docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt b/docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt deleted file mode 100644 index fb360e970..000000000 --- a/docs/results/leader-block-packing/2026-09-13/baseline-capacity.txt +++ /dev/null @@ -1 +0,0 @@ -ok github.com/Overclock-Validator/mithril/pkg/blockprod 1.076s diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json b/docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json deleted file mode 100644 index d635f286e..000000000 --- a/docs/results/leader-block-packing/2026-09-13/best-block/all-finalized-costs.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "stage": "window-fill", - "finalized_load_transactions": 200000, - "not_included_after_expiry": 0, - "blocks": [ - { - "slot": 3073656, - "transactions": 31732, - "load_transactions": 31732, - "cost_units": 32620496, - "failed_transactions": 0 - }, - { - "slot": 3073657, - "transactions": 48523, - "load_transactions": 48523, - "cost_units": 49881644, - "failed_transactions": 0 - }, - { - "slot": 3073658, - "transactions": 39911, - "load_transactions": 39745, - "cost_units": 41103706, - "failed_transactions": 0 - }, - { - "slot": 3073659, - "transactions": 138, - "load_transactions": 0, - "cost_units": 204378, - "failed_transactions": 0 - }, - { - "slot": 3073740, - "transactions": 34188, - "load_transactions": 34150, - "cost_units": 35162478, - "failed_transactions": 0 - }, - { - "slot": 3073741, - "transactions": 45094, - "load_transactions": 45094, - "cost_units": 46356632, - "failed_transactions": 0 - }, - { - "slot": 3073742, - "transactions": 852, - "load_transactions": 756, - "cost_units": 919344, - "failed_transactions": 0 - } - ] -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json b/docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json deleted file mode 100644 index 85a32860b..000000000 --- a/docs/results/leader-block-packing/2026-09-13/best-block/largest-block-metrics.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "slot": 3073657, - "transactions": 48523, - "load_transactions": 48523, - "block_cost_units": 49881644, - "execution_cu": 0, - "failed_transactions": 0, - "load_cost_units_each": [ - 1028 - ], - "load_execution_cu_each": [ - 0 - ], - "finalized": true, - "blockhash": "H6iU43SrXp94LRvfpvhLDskboUJA4CupsEVFJcCRLANF" -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/metadata.json b/docs/results/leader-block-packing/2026-09-13/best-block/metadata.json deleted file mode 100644 index d625573fc..000000000 --- a/docs/results/leader-block-packing/2026-09-13/best-block/metadata.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "blockhash": "4X7hVhH1RJReoEkJcFQSV2BTKbjFQT8JK16dD3bEEwuv", - "count": 200000, - "last_valid_block_height": 2839082, - "offered_tps": 150000, - "prefill_count": 120000, - "prefill_slots": 10, - "prepare_seconds": 2.715821878, - "sender": "127.0.0.1:8011; GOMAXPROCS=2; signatures prepared before send", - "shape": "readonly-pair", - "signatures_per_transaction": 1, - "topup_blockhash": "DTSw2jqMLS6A3yCuJ27d7BpH5sWttE53sy6Pw6FMwWie", - "topup_count": 80000, - "topup_trigger": "first local block broadcast", - "transaction_bytes": 198, - "window": 3073656 -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/send.json b/docs/results/leader-block-packing/2026-09-13/best-block/send.json deleted file mode 100644 index 4f4fd60e9..000000000 --- a/docs/results/leader-block-packing/2026-09-13/best-block/send.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "end_slot": 3073659, - "end_utc": "2026-09-13T06:45:12.217473403Z", - "first_error": "", - "not_sent": 0, - "send_errors": 0, - "send_seconds": 3.291255092, - "sent": 200000, - "start_utc": "2026-09-13T06:45:08.926218321Z", - "target_window": 3073656, - "topup_start_utc": "2026-09-13T06:45:11.683951471Z" -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/best-block/validation.json b/docs/results/leader-block-packing/2026-09-13/best-block/validation.json deleted file mode 100644 index 6af787f42..000000000 --- a/docs/results/leader-block-packing/2026-09-13/best-block/validation.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "utc": "2026-09-13T06:48:40.355016+00:00", - "stage": "window-fill", - "blocks": [ - { - "slot": 3073656, - "blockhash": "6CGAhXwTZSMsS5Uda6CD19VRqaYeBUQoodZJW7X9PXcx", - "alpenglow_block_id": "8xkgbbwhcXujEMbn1bKhkWvTEiEwBfoPZVkTNs4ihcgj", - "certificate_logs": [ - "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073656 rank=21 certificate=notarize block=8xkgbbwhcXujEMbn1bKhkWvTEiEwBfoPZVkTNs4ihcgj network_landed=8654" - ], - "finalized": true, - "finalization_ms": 7, - "deadline_margin_ms": 50, - "transactions": 31732, - "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073656 terminal=complete cause=complete reason=complete replay_frontier=3073651 live_slot=3073655 block=8xkgbbwhcXujEMbn1bKhkWvTEiEwBfoPZVkTNs4ihcgj parent_slot=3073651 txns=31732 window_elapsed_ms=149 finalization_ms=7 deadline_margin_ms=50 local_handoff_ms=0" - }, - { - "slot": 3073657, - "blockhash": "H6iU43SrXp94LRvfpvhLDskboUJA4CupsEVFJcCRLANF", - "alpenglow_block_id": "AQi4tKfzW3MRNHGhndxpL9sq6QBr5aoyR22ZPBA8RJUy", - "certificate_logs": [ - "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073657 rank=21 certificate=notarize block=AQi4tKfzW3MRNHGhndxpL9sq6QBr5aoyR22ZPBA8RJUy network_landed=8656" - ], - "finalized": true, - "finalization_ms": 9, - "deadline_margin_ms": 49, - "transactions": 48523, - "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073657 terminal=complete cause=complete reason=complete replay_frontier=3073656 live_slot=3073656 block=AQi4tKfzW3MRNHGhndxpL9sq6QBr5aoyR22ZPBA8RJUy parent_slot=3073656 txns=48523 window_elapsed_ms=350 finalization_ms=9 deadline_margin_ms=49 local_handoff_ms=0" - }, - { - "slot": 3073658, - "blockhash": "939M6Zfyuj9wwyrMVnzhF5WmyqzpSZfUQJwBA5gk2jto", - "alpenglow_block_id": "EWr2Nh8MFMUMQXvLsPAW6Z3xSPcuFP8efDrxjYtdz9s5", - "certificate_logs": [ - "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073658 rank=21 certificate=notarize-fallback block=EWr2Nh8MFMUMQXvLsPAW6Z3xSPcuFP8efDrxjYtdz9s5 network_landed=8658" - ], - "finalized": true, - "finalization_ms": 8, - "deadline_margin_ms": 49, - "transactions": 39911, - "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073658 terminal=complete cause=complete reason=complete replay_frontier=3073657 live_slot=3073657 block=EWr2Nh8MFMUMQXvLsPAW6Z3xSPcuFP8efDrxjYtdz9s5 parent_slot=3073657 txns=39911 window_elapsed_ms=550 finalization_ms=8 deadline_margin_ms=49 local_handoff_ms=0" - }, - { - "slot": 3073659, - "blockhash": "EXX1YCesL18q3KAUEehr4WsDZPcuSd2ErYKatMPBSDdS", - "alpenglow_block_id": "2JUsPbPn4JeyUhGkXqersNaYyKHZJut4NDgp6sFkarXe", - "certificate_logs": [ - "(+17m19s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073659 rank=21 certificate=notarize-fallback block=2JUsPbPn4JeyUhGkXqersNaYyKHZJut4NDgp6sFkarXe network_landed=8659" - ], - "finalized": true, - "finalization_ms": 10, - "deadline_margin_ms": 47, - "transactions": 138, - "log": "(+17m19s) ALPENGLOW block production: broadcast local leader slot=3073659 terminal=complete cause=complete reason=complete replay_frontier=3073658 live_slot=3073658 block=2JUsPbPn4JeyUhGkXqersNaYyKHZJut4NDgp6sFkarXe parent_slot=3073658 txns=138 window_elapsed_ms=752 finalization_ms=10 deadline_margin_ms=47 local_handoff_ms=0" - }, - { - "slot": 3073740, - "blockhash": "saQYhwkvXUsuqa9xN2gE1cGMPZpAPJmB4fTZWYrQ6wf", - "alpenglow_block_id": "CzcvprhgxhmS4uU4Xxav9g68aBozMt9KTj22SFxF6fNi", - "certificate_logs": [ - "(+17m37s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073740 rank=21 certificate=notarize block=CzcvprhgxhmS4uU4Xxav9g68aBozMt9KTj22SFxF6fNi network_landed=8804" - ], - "finalized": true, - "finalization_ms": 10, - "deadline_margin_ms": 49, - "transactions": 34188, - "log": "(+17m37s) ALPENGLOW block production: broadcast local leader slot=3073740 terminal=complete cause=complete reason=complete replay_frontier=3073735 live_slot=3073739 block=CzcvprhgxhmS4uU4Xxav9g68aBozMt9KTj22SFxF6fNi parent_slot=3073735 txns=34188 window_elapsed_ms=150 finalization_ms=10 deadline_margin_ms=49 local_handoff_ms=0" - }, - { - "slot": 3073741, - "blockhash": "BN1bpbetrHiU2tzm4DCam4T9F8ZisDkgDPnqA35S9gFi", - "alpenglow_block_id": "146JxvWuGXKR1wvsA4vmosPDMNVoEBEoD93u6fv1abWX", - "certificate_logs": [ - "(+17m37s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073741 rank=21 certificate=notarize block=146JxvWuGXKR1wvsA4vmosPDMNVoEBEoD93u6fv1abWX network_landed=8806" - ], - "finalized": true, - "finalization_ms": 10, - "deadline_margin_ms": 49, - "transactions": 45094, - "log": "(+17m37s) ALPENGLOW block production: broadcast local leader slot=3073741 terminal=complete cause=complete reason=complete replay_frontier=3073740 live_slot=3073740 block=146JxvWuGXKR1wvsA4vmosPDMNVoEBEoD93u6fv1abWX parent_slot=3073740 txns=45094 window_elapsed_ms=350 finalization_ms=10 deadline_margin_ms=49 local_handoff_ms=0" - }, - { - "slot": 3073742, - "blockhash": "AFytPNQRtXwRbWwnzi6D9aAVz55h43KknkHTJUWnqtr7", - "alpenglow_block_id": "3STHfQmzAywmCDRJuosJ6U6ASHJZxeXpRxaBnTfhiSEb", - "certificate_logs": [ - "(+17m37s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3073742 rank=21 certificate=notarize block=3STHfQmzAywmCDRJuosJ6U6ASHJZxeXpRxaBnTfhiSEb network_landed=8808" - ], - "finalized": true, - "finalization_ms": 9, - "deadline_margin_ms": 48, - "transactions": 852, - "log": "(+17m37s) ALPENGLOW block production: broadcast local leader slot=3073742 terminal=complete cause=complete reason=complete replay_frontier=3073741 live_slot=3073741 block=3STHfQmzAywmCDRJuosJ6U6ASHJZxeXpRxaBnTfhiSEb parent_slot=3073741 txns=852 window_elapsed_ms=551 finalization_ms=9 deadline_margin_ms=48 local_handoff_ms=0" - } - ], - "log_errors": [], - "included": 200000 -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/binary-sha256.txt b/docs/results/leader-block-packing/2026-09-13/binary-sha256.txt deleted file mode 100644 index f9451ea62..000000000 --- a/docs/results/leader-block-packing/2026-09-13/binary-sha256.txt +++ /dev/null @@ -1,3 +0,0 @@ -302484874e65f2b01265ecf1f7c4ea3f8e850077844e795ef2a6bea55749ef6c /srv/mithril-leader-packing-pr-20260913/results/baseline.test -02395f246f80ce6a3e337000dc030dec1e04daf35a7149db4481f20587f406dc /srv/mithril-leader-packing-pr-20260913/results/candidate.test -230f6474e438ef29623537bef8aec6d5fa9fbb14014313231f132bf8e78a445f /srv/mithril-leader-packing-pr-20260913/results/mithril diff --git a/docs/results/leader-block-packing/2026-09-13/evidence-manifest.json b/docs/results/leader-block-packing/2026-09-13/evidence-manifest.json deleted file mode 100644 index 6091e4c10..000000000 --- a/docs/results/leader-block-packing/2026-09-13/evidence-manifest.json +++ /dev/null @@ -1,52 +0,0 @@ -[ - { - "source": "leader-window-fill-20260913/evidence/load/window-fill/validation.json", - "path": "best-block/validation.json", - "sha256": "a6c503c254c76138da8142c81cee665f31338f6bff756c77c4e9f5b85229b1c5" - }, - { - "source": "leader-window-fill-20260913/evidence/load/window-fill/largest-block-metrics.json", - "path": "best-block/largest-block-metrics.json", - "sha256": "733203a4bb62a7fd28f92b73ff0bdfb6592855d9aef2ece3f4447b4f45ac7c54" - }, - { - "source": "leader-window-fill-20260913/evidence/load/window-fill/all-finalized-costs.json", - "path": "best-block/all-finalized-costs.json", - "sha256": "f310f47b66bf49d14272109c3a97dc9c5f73ee740edac67aea783a38cffbf897" - }, - { - "source": "leader-window-fill-20260913/evidence/load/window-fill/metadata.json", - "path": "best-block/metadata.json", - "sha256": "265ca56fd39ff78b8d4ff2f2643f4e5400db69b4b32e6ffc5e027d3dbe3a1203" - }, - { - "source": "leader-window-fill-20260913/evidence/load/window-fill/send.json", - "path": "best-block/send.json", - "sha256": "0ebdc42dd32bee8085f1147512546b7aef851cd0a6e7e03cb45d0c22a818ae05" - }, - { - "source": "leader-window-prefill-20260913/evidence/load/window-fill/validation.json", - "path": "four-block-prefill/validation.json", - "sha256": "2223cb7bdf044ff9e23c6b08cfb40c1f428010de0f591322035b4574733567d0" - }, - { - "source": "leader-window-prefill-20260913/evidence/load/window-fill/largest-block-metrics.json", - "path": "four-block-prefill/largest-block-metrics.json", - "sha256": "8f3d6c54334eb3a0fec8568425ebe4895966ca756534fc5c8bbb6f40ab6af20e" - }, - { - "source": "leader-window-prefill-20260913/evidence/load/window-fill/all-finalized-costs.json", - "path": "four-block-prefill/all-finalized-costs.json", - "sha256": "3f46aa6eb8fe6e0d6d738a7cc7026780ae4135bd1bee54f5a13ef3a5f6ccd14c" - }, - { - "source": "leader-window-prefill-20260913/evidence/load/window-fill/metadata.json", - "path": "four-block-prefill/metadata.json", - "sha256": "2e6c576474cd573124700a9abb8b40429d8c14092259ad50cf47c3d4bce0dea4" - }, - { - "source": "leader-window-prefill-20260913/evidence/load/window-fill/send.json", - "path": "four-block-prefill/send.json", - "sha256": "cddce7d90b8d039b06167ad5d845a0a0388fe678f95f5775bd08e9dd314643b9" - } -] diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json deleted file mode 100644 index ed4e70971..000000000 --- a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/all-finalized-costs.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "stage": "window-fill", - "finalized_load_transactions": 152831, - "not_included_after_expiry": 47169, - "blocks": [ - { - "slot": 3077076, - "transactions": 26687, - "load_transactions": 26687, - "cost_units": 27434236, - "failed_transactions": 0 - }, - { - "slot": 3077077, - "transactions": 42150, - "load_transactions": 42150, - "cost_units": 43330200, - "failed_transactions": 0 - }, - { - "slot": 3077078, - "transactions": 41387, - "load_transactions": 41387, - "cost_units": 42545836, - "failed_transactions": 0 - }, - { - "slot": 3077079, - "transactions": 42607, - "load_transactions": 42607, - "cost_units": 43799996, - "failed_transactions": 0 - } - ] -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json deleted file mode 100644 index 22e907bc6..000000000 --- a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/largest-block-metrics.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "slot": 3077079, - "transactions": 42607, - "load_transactions": 42607, - "block_cost_units": 43799996, - "execution_cu": 0, - "failed_transactions": 0, - "load_cost_units_each": [ - 1028 - ], - "load_execution_cu_each": [ - 0 - ], - "finalized": true, - "blockhash": "FXcMHsNzG53SnuW1kR2ZzxGLx6p9FxeUsgeLvMzS1rhL" -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json deleted file mode 100644 index 1a321b46d..000000000 --- a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/metadata.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "blockhash": "Cjef4Kov7MMcdEeY2vUrfT2pineEzykVvsy6321CPbtE", - "count": 200000, - "last_valid_block_height": 2842026, - "offered_tps": 150000, - "prefill_count": 200000, - "prefill_slots": 20, - "prepare_seconds": 2.571031937, - "sender": "127.0.0.1:8011; GOMAXPROCS=2; signatures prepared before send", - "shape": "readonly-pair", - "signatures_per_transaction": 1, - "topup_blockhash": "GUgh5AT5Ci7p2Et6GzZFscesHLQ7gVmmpHyzhuutcvQx", - "topup_count": 0, - "topup_trigger": "none; entire workload preloaded", - "transaction_bytes": 198, - "window": 3077076 -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json deleted file mode 100644 index 2656f8f7f..000000000 --- a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/send.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "end_slot": 3077062, - "end_utc": "2026-09-13T06:57:13.621742904Z", - "first_error": "", - "not_sent": 0, - "send_errors": 0, - "send_seconds": 1.333626813, - "sent": 200000, - "start_utc": "2026-09-13T06:57:12.288116091Z", - "target_window": 3077076 -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json b/docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json deleted file mode 100644 index d571065a2..000000000 --- a/docs/results/leader-block-packing/2026-09-13/four-block-prefill/validation.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "utc": "2026-09-13T06:58:15.837757+00:00", - "stage": "window-fill", - "blocks": [ - { - "slot": 3077076, - "blockhash": "6WQ6hRYS9KLCsAWcvvGuwRujFL8uUH6AX28wUrxCAPfw", - "alpenglow_block_id": "A3cMotBW2NT7LaGk1NgEQ7RinuWAeVwNiAeCZQ2GYpYA", - "certificate_logs": [ - "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077076 rank=21 certificate=notarize block=A3cMotBW2NT7LaGk1NgEQ7RinuWAeVwNiAeCZQ2GYpYA network_landed=553" - ], - "finalized": true, - "finalization_ms": 15, - "deadline_margin_ms": 39, - "transactions": 26687, - "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077076 terminal=complete cause=complete reason=complete replay_frontier=3077075 live_slot=3077077 block=A3cMotBW2NT7LaGk1NgEQ7RinuWAeVwNiAeCZQ2GYpYA parent_slot=3077075 txns=26687 window_elapsed_ms=160 finalization_ms=15 deadline_margin_ms=39 local_handoff_ms=0" - }, - { - "slot": 3077077, - "blockhash": "A4DT5t7mxKQ7Udb4ixU7dLiww9feWkPHswGzGyFCe5at", - "alpenglow_block_id": "8tMUnRr5rik2yiKdwxJnKyLLDsc5vyn6Q9vT7jazvvsX", - "certificate_logs": [ - "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077077 rank=21 certificate=notarize block=8tMUnRr5rik2yiKdwxJnKyLLDsc5vyn6Q9vT7jazvvsX network_landed=555" - ], - "finalized": true, - "finalization_ms": 14, - "deadline_margin_ms": 45, - "transactions": 42150, - "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077077 terminal=complete cause=complete reason=complete replay_frontier=3077076 live_slot=3077076 block=8tMUnRr5rik2yiKdwxJnKyLLDsc5vyn6Q9vT7jazvvsX parent_slot=3077076 txns=42150 window_elapsed_ms=354 finalization_ms=14 deadline_margin_ms=45 local_handoff_ms=0" - }, - { - "slot": 3077078, - "blockhash": "A48aPKzQzo5P7TN4qWn3hv1CwK8yRLvsCXPXnc3J6Yyj", - "alpenglow_block_id": "5FhWEtjG9WGj11z1ZkD5Gx3jYAkVaPytx1PPacQ6bR4H", - "certificate_logs": [ - "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077078 rank=21 certificate=notarize block=5FhWEtjG9WGj11z1ZkD5Gx3jYAkVaPytx1PPacQ6bR4H network_landed=557" - ], - "finalized": true, - "finalization_ms": 13, - "deadline_margin_ms": 41, - "transactions": 41387, - "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077078 terminal=complete cause=complete reason=complete replay_frontier=3077077 live_slot=3077077 block=5FhWEtjG9WGj11z1ZkD5Gx3jYAkVaPytx1PPacQ6bR4H parent_slot=3077077 txns=41387 window_elapsed_ms=558 finalization_ms=13 deadline_margin_ms=41 local_handoff_ms=0" - }, - { - "slot": 3077079, - "blockhash": "FXcMHsNzG53SnuW1kR2ZzxGLx6p9FxeUsgeLvMzS1rhL", - "alpenglow_block_id": "ChYL4dneUd22GQc3YxFafh7Lc1XvEdCTFPLLaAhAVDDQ", - "certificate_logs": [ - "(+ 1m13s) ALPENGLOW voting: vote landed source=votor-quic proof=verified-aggregate vote=notarize slot=3077079 rank=21 certificate=notarize-fallback block=ChYL4dneUd22GQc3YxFafh7Lc1XvEdCTFPLLaAhAVDDQ network_landed=559" - ], - "finalized": true, - "finalization_ms": 13, - "deadline_margin_ms": 41, - "transactions": 42607, - "log": "(+ 1m13s) ALPENGLOW block production: broadcast local leader slot=3077079 terminal=complete cause=complete reason=complete replay_frontier=3077078 live_slot=3077078 block=ChYL4dneUd22GQc3YxFafh7Lc1XvEdCTFPLLaAhAVDDQ parent_slot=3077078 txns=42607 window_elapsed_ms=758 finalization_ms=13 deadline_margin_ms=41 local_handoff_ms=0" - } - ], - "log_errors": [], - "included": 152831 -} \ No newline at end of file diff --git a/docs/results/leader-block-packing/2026-09-13/live-preload.go.txt b/docs/results/leader-block-packing/2026-09-13/live-preload.go.txt deleted file mode 100644 index ca979fa37..000000000 --- a/docs/results/leader-block-packing/2026-09-13/live-preload.go.txt +++ /dev/null @@ -1,510 +0,0 @@ -// Bounded, own-validator-only Alpenglow load experiment. Not production code. -package main - -import ( - "bytes" - "context" - "crypto/ed25519" - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "encoding/binary" - "encoding/json" - "flag" - "fmt" - "net/http" - "os" - "path/filepath" - "sync" - "sync/atomic" - "time" - - "github.com/Overclock-Validator/mithril/pkg/tpu/quicserver/testutils" - "github.com/gagliardetto/solana-go" -) - -const identity = "AzDzG3imFz5ihvetCcGixiwuieH7jfsaivnGDXgZo6Z8" -const genesis = "D5SkuirXeXeUTR7z2A65nzgU1GZ6uJ4vzf2tCy3Zt88b" -const rpcURL = "https://rpc.ag.validator1.net" - -var root = flag.String("root", "/srv/mithril-leader-window-prefill-20260913/load", "private experiment directory") -var mode = flag.String("mode", "selfcheck", "init, selfcheck, simulate, or run") -var count = flag.Int("count", 32, "unique transactions; hard limit 200000") -var rate = flag.Int("rate", 100, "offered transactions/s; hard limit 150000") -var stage = flag.String("stage", "pilot", "unique result directory name") -var prefill = flag.Int("prefill-slots", 0, "send this many slots before leadership (0..20)") -var shape = flag.String("shape", "readonly-pair", "account-nonce, compute-budget or readonly-pair") -var transport = &http.Client{Timeout: 15 * time.Second} - -func must(err error) { - if err != nil { - panic(err) - } -} -func logEvent(v any) { b, e := json.Marshal(v); must(e); fmt.Println(string(b)) } -func save(path string, v any) { - b, e := json.MarshalIndent(v, "", " ") - must(e) - must(os.WriteFile(path, b, 0600)) -} -func rpc(method string, params any, out any) error { - b, e := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": method, "params": params}) - if e != nil { - return e - } - r, e := transport.Post(rpcURL, "application/json", bytes.NewReader(b)) - if e != nil { - return e - } - defer r.Body.Close() - var env struct { - Result json.RawMessage `json:"result"` - Error json.RawMessage `json:"error"` - } - if e = json.NewDecoder(r.Body).Decode(&env); e != nil { - return e - } - if len(env.Error) > 0 && string(env.Error) != "null" { - return fmt.Errorf("RPC %s: %s", method, env.Error) - } - return json.Unmarshal(env.Result, out) -} -func keyPath(i int) string { return filepath.Join(*root, "keys", fmt.Sprintf("payer-%d.json", i)) } -func initKeys() { - must(os.MkdirAll(filepath.Join(*root, "keys"), 0700)) - var pubs []string - for i := 0; i < 8; i++ { - _, p, e := ed25519.GenerateKey(rand.Reader) - must(e) - vals := make([]int, len(p)) - for j, b := range p { - vals[j] = int(b) - } - b, e := json.Marshal(vals) - must(e) - f, e := os.OpenFile(keyPath(i), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) - must(e) - _, e = f.Write(b) - must(e) - must(f.Close()) - pubs = append(pubs, solana.PublicKeyFromBytes(p.Public().(ed25519.PublicKey)).String()) - } - save(filepath.Join(*root, "payers.json"), pubs) - logEvent(map[string]any{"payers": pubs}) -} -func loadKeys() []ed25519.PrivateKey { - keys := make([]ed25519.PrivateKey, 8) - for i := range keys { - b, e := os.ReadFile(keyPath(i)) - must(e) - var a []int - must(json.Unmarshal(b, &a)) - if len(a) != 64 { - panic("invalid key length") - } - p := make([]byte, 64) - for j, v := range a { - if v < 0 || v > 255 { - panic("invalid key byte") - } - p[j] = byte(v) - } - keys[i] = ed25519.PrivateKey(p) - } - return keys -} -func pairIndices(ordinal int) (int, int) { - if ordinal < 0 || ordinal >= 128*127 { - panic("pair ordinal outside pool capacity") - } - // A permutation distributes early transactions over the fixed account pool. - n := (ordinal * 7919) % (128 * 127) - a, b := n/127, n%127 - if b >= a { - b++ - } - return a, b -} -func loadPool() []solana.PublicKey { - b, e := os.ReadFile(filepath.Join(*root, "pool.json")) - must(e) - var names []string - must(json.Unmarshal(b, &names)) - if len(names) != 128 { - panic("wrong pool length") - } - p := make([]solana.PublicKey, 128) - seen := map[solana.PublicKey]bool{} - for i, n := range names { - p[i] = solana.MustPublicKeyFromBase58(n) - if seen[p[i]] { - panic("duplicate pool account") - } - seen[p[i]] = true - } - return p -} -func makePairWire(key ed25519.PrivateKey, hash solana.Hash, pool []solana.PublicKey, ordinal int) []byte { - a, b := pairIndices(ordinal) - payer := solana.PublicKeyFromBytes(key.Public().(ed25519.PublicKey)) - if payer == pool[a] || payer == pool[b] { - panic("payer must be distinct from readonly pool") - } - msg := []byte{1, 0, 2, 3} - msg = append(msg, payer[:]...) - msg = append(msg, pool[a][:]...) - msg = append(msg, pool[b][:]...) - msg = append(msg, hash[:]...) - msg = append(msg, 0) - w := []byte{1} - w = append(w, ed25519.Sign(key, msg)...) - return append(w, msg...) -} - -func makeWire(key ed25519.PrivateKey, hash solana.Hash, nonce [32]byte) []byte { - // One writable signer, one unique readonly unsigned account, no instructions. - msg := []byte{1, 0, 1, 2} - msg = append(msg, key.Public().(ed25519.PublicKey)...) - msg = append(msg, nonce[:]...) - msg = append(msg, hash[:]...) - msg = append(msg, 0) - sig := ed25519.Sign(key, msg) - w := []byte{1} - w = append(w, sig...) - return append(w, msg...) -} -func makeBudgetWire(key ed25519.PrivateKey, hash solana.Hash, limit uint32) []byte { - program := solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111") - msg := []byte{1, 0, 1, 2} - msg = append(msg, key.Public().(ed25519.PublicKey)...) - msg = append(msg, program[:]...) - msg = append(msg, hash[:]...) - msg = append(msg, 1, 1, 0, 5, 2) // one instruction: program 1, no accounts, 5 data bytes, SetComputeUnitLimit - msg = binary.LittleEndian.AppendUint32(msg, limit) - w := []byte{1} - w = append(w, ed25519.Sign(key, msg)...) - return append(w, msg...) -} -func check(w []byte) { - t, e := solana.TransactionFromBytes(w) - must(e) - if (len(w) != 166 && len(w) != 174 && len(w) != 198) || len(t.Signatures) != 1 || len(t.Message.Instructions) > 1 { - panic("wrong transaction shape") - } - if !ed25519.Verify(ed25519.PublicKey(t.Message.AccountKeys[0][:]), w[65:], w[1:65]) { - panic("invalid signature") - } -} -func getSlot() uint64 { - var s uint64 - must(rpc("getSlot", []any{map[string]any{"commitment": "processed"}}, &s)) - return s -} -func getHash() (solana.Hash, uint64) { - var r struct { - Value struct { - Blockhash string - LastValidBlockHeight uint64 - } - } - must(rpc("getLatestBlockhash", []any{map[string]any{"commitment": "confirmed"}}, &r)) - h, e := solana.HashFromBase58(r.Value.Blockhash) - must(e) - return h, r.Value.LastValidBlockHeight -} -func selectWindow() uint64 { - var e struct{ AbsoluteSlot, SlotIndex uint64 } - must(rpc("getEpochInfo", []any{}, &e)) - var sched map[string][]uint64 - must(rpc("getLeaderSchedule", []any{nil, map[string]any{"identity": identity}}, &sched)) - base := e.AbsoluteSlot - e.SlotIndex - for _, s := range sched[identity] { - abs := base + s - if abs%4 == 0 && abs > e.AbsoluteSlot+65 { - return abs - } - } - panic("no suitable leader window in this epoch") -} -func awaitSlot(target uint64) uint64 { - deadline := time.Now().Add(10 * time.Minute) - for time.Now().Before(deadline) { - s := getSlot() - if s >= target { - return s - } - if target-s > 30 { - time.Sleep(2 * time.Second) - } else { - time.Sleep(75 * time.Millisecond) - } - } - panic("wait deadline") -} -func makePhasedPairWire(keys []ed25519.PrivateKey, first, second solana.Hash, pool []solana.PublicKey, i int) []byte { - if i < 0 || i >= 200000 || first == second { - panic("invalid phased load input") - } - txHash, ordinal := first, i/8 - if i >= 120000 { - txHash, ordinal = second, (i-120000)/8 - } - return makePairWire(keys[i%8], txHash, pool, ordinal) -} - -func main() { - flag.Parse() - if *shape != "readonly-pair" && *shape != "account-nonce" && *shape != "compute-budget" { - panic("invalid shape") - } - if *mode == "init" { - initKeys() - return - } - if *count < 1 || *count > 200000 || *rate < 1 || *rate > 150000 || *prefill < 0 || *prefill > 20 { - panic("outside experiment bounds") - } - if filepath.Base(*stage) != *stage { - panic("invalid stage") - } - if *mode == "selfcheck" { - _, p, e := ed25519.GenerateKey(rand.Reader) - must(e) - w := makeWire(p, solana.Hash{}, sha256.Sum256([]byte("test"))) - check(w) - check(makeBudgetWire(p, solana.Hash{}, 1000)) - pool := make([]solana.PublicKey, 128) - for i := range pool { - pool[i] = solana.PublicKey{byte(i + 1), 77} - } - seen := map[[64]byte]bool{} - for i := 0; i < 128*127; i++ { - a, b := pairIndices(i) - var pair [64]byte - copy(pair[:32], pool[a][:]) - copy(pair[32:], pool[b][:]) - if a == b || seen[pair] { - panic("pair collision") - } - seen[pair] = true - } - for _, i := range []int{0, 1, 126, 127, 14999, 16255} { - check(makePairWire(p, solana.Hash{}, pool, i)) - } - logEvent(map[string]any{"wire_bytes": 198, "signatures": 1, "signature_verified": true, "unique_pairs": len(seen)}) - return - } - var g string - must(rpc("getGenesisHash", []any{}, &g)) - if g != genesis { - panic("unexpected cluster genesis") - } - keys := loadKeys() - pool := loadPool() - if *mode == "simulate" { - h, _ := getHash() - w := makeWire(keys[0], h, sha256.Sum256([]byte("simulation"))) - if *shape == "compute-budget" { - w = makeBudgetWire(keys[0], h, 1000) - } - if *shape == "readonly-pair" { - w = makePairWire(keys[0], h, pool, 14999) - } - check(w) - var sim any - must(rpc("simulateTransaction", []any{base64.StdEncoding.EncodeToString(w), map[string]any{"encoding": "base64", "sigVerify": true, "commitment": "confirmed"}}, &sim)) - save(filepath.Join(*root, *shape+"-simulation.json"), sim) - logEvent(sim) - return - } - if *mode != "run" { - panic("unknown mode") - } - // Claim the stage before signing/sending. No automatic replay of uncertain runs. - out := filepath.Join(*root, *stage) - must(os.Mkdir(out, 0700)) - // Bound all experiment sends by a durable count, including uncertain prior runs. - ledger := filepath.Join(*root, "authorized-count.json") - used := 0 - if b, e := os.ReadFile(ledger); e == nil { - must(json.Unmarshal(b, &used)) - } else if !os.IsNotExist(e) { - must(e) - } - if used+*count > 200000 { - panic("cumulative maximum fee 1.0 testnet SOL exceeded") - } - save(ledger, used+*count) - window := selectWindow() - logEvent(map[string]any{"event": "waiting_to_prepare", "leader_window": window, "now_slot": getSlot(), "count": *count, "offered_tps": *rate}) - awaitSlot(window - 55) - hash, lastHeight := getHash() - hashLedger := filepath.Join(*root, "used-blockhashes.json") - usedHashes := map[string]bool{} - if data, err := os.ReadFile(hashLedger); err == nil { - must(json.Unmarshal(data, &usedHashes)) - } else if !os.IsNotExist(err) { - must(err) - } - if usedHashes[hash.String()] { - panic("blockhash already used by this experiment") - } - usedHashes[hash.String()] = true - save(hashLedger, usedHashes) - secondHash, secondHeight := hash, lastHeight - if *count > 120000 { - for attempts := 0; attempts < 40; attempts++ { - time.Sleep(100 * time.Millisecond) - secondHash, secondHeight = getHash() - if secondHash != hash { - break - } - } - if secondHash == hash || usedHashes[secondHash.String()] { - panic("no fresh top-up blockhash") - } - usedHashes[secondHash.String()] = true - save(hashLedger, usedHashes) - } - nonce := make([]byte, 32) - _, e := rand.Read(nonce) - must(e) - wires := make([][]byte, *count) - next := atomic.Int64{} - var wg sync.WaitGroup - beg := time.Now() - for k := 0; k < 4; k++ { - wg.Add(1) - go func() { - defer wg.Done() - for { - i := int(next.Add(1) - 1) - if i >= len(wires) { - return - } - n := append([]byte(nil), nonce...) - n = binary.LittleEndian.AppendUint64(n, uint64(i)) - wires[i] = makeWire(keys[i%8], hash, sha256.Sum256(n)) - if *shape == "compute-budget" { - wires[i] = makeBudgetWire(keys[i%8], hash, uint32(1000+i)) - } - if *shape == "readonly-pair" { - if len(wires) > 120000 { - wires[i] = makePhasedPairWire(keys, hash, secondHash, pool, i) - } else { - wires[i] = makePairWire(keys[i%8], hash, pool, i/8) - } - } - } - }() - } - wg.Wait() - check(wires[0]) - check(wires[len(wires)-1]) - if len(wires) > 120000 { - check(wires[120000]) - } - ids := make([]string, len(wires)) - for i, w := range wires { - ids[i] = solana.SignatureFromBytes(w[1:65]).String() - } - save(filepath.Join(out, "signatures.json"), ids) - meta := map[string]any{"window": window, "count": *count, "offered_tps": *rate, "transaction_bytes": len(wires[0]), "shape": *shape, "signatures_per_transaction": 1, "prefill_slots": *prefill, "blockhash": hash.String(), "last_valid_block_height": max(lastHeight, secondHeight), "topup_blockhash": secondHash.String(), "prefill_count": *count, "topup_count": 0, "topup_trigger": "none; entire workload preloaded", "prepare_seconds": time.Since(beg).Seconds(), "sender": "127.0.0.1:8011; GOMAXPROCS=2; signatures prepared before send"} - save(filepath.Join(out, "metadata.json"), meta) - if getSlot() >= window-uint64(*prefill) { - panic("missed preparation deadline; no sends") - } - client, e := testutils.NewClient() - must(e) - // Four connections and four streams each respect the validator's existing limits. - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - type senderFn func(context.Context, []byte) error - senders := make([]senderFn, 4) - var closers []func() - for i := range senders { - c, e := client.Dial(ctx, "127.0.0.1:8011") - must(e) - senders[i] = func(cctx context.Context, w []byte) error { return client.Send(cctx, c, w) } - closers = append(closers, func() { _ = c.CloseWithError(0, "") }) - } - defer func() { - for _, f := range closers { - f() - } - }() - observed := awaitSlot(window - uint64(*prefill)) - if observed > window+1 { - panic("missed send window; no sends") - } - var height uint64 - must(rpc("getBlockHeight", []any{map[string]any{"commitment": "confirmed"}}, &height)) - if height+10 >= lastHeight { - panic("blockhash too old") - } - logEvent(map[string]any{"event": "sending", "observed_slot": observed, "leader_window": window}) - start := time.Now() - jobs := make(chan int, 512) - var sent, errors, skipped atomic.Uint64 - var firstErr string - var errMu sync.Mutex - var endMu sync.Mutex - lastSend := start - sendCtx, stop := context.WithTimeout(context.Background(), 3*time.Second) - defer stop() - for k := 0; k < 16; k++ { - wg.Add(1) - go func(worker int) { - defer wg.Done() - for i := range jobs { - if sendCtx.Err() != nil { - skipped.Add(1) - continue - } - e := senders[worker%4](sendCtx, wires[i]) - if e != nil { - errors.Add(1) - errMu.Lock() - if firstErr == "" { - firstErr = e.Error() - } - errMu.Unlock() - } else { - sent.Add(1) - endMu.Lock() - lastSend = time.Now() - endMu.Unlock() - } - } - }(k) - } - for i := 0; i < len(wires); { - - if sendCtx.Err() != nil { - skipped.Add(uint64(len(wires) - i)) - break - } - end := i + 150 - if end > len(wires) { - end = len(wires) - } - target := start.Add(time.Duration(float64(i) / float64(*rate) * float64(time.Second))) - if d := time.Until(target); d > 0 { - time.Sleep(d) - } - for ; i < end; i++ { - select { - case jobs <- i: - case <-sendCtx.Done(): - skipped.Add(1) - } - } - } - close(jobs) - wg.Wait() - summary := map[string]any{"sent": sent.Load(), "send_errors": errors.Load(), "not_sent": skipped.Load(), "first_error": firstErr, "start_utc": start.UTC().Format(time.RFC3339Nano), "end_utc": lastSend.UTC().Format(time.RFC3339Nano), "send_seconds": lastSend.Sub(start).Seconds(), "end_slot": getSlot(), "target_window": window} - save(filepath.Join(out, "send.json"), summary) - logEvent(summary) - // Let already-written stream data be acknowledged before closing QUIC connections. - time.Sleep(500 * time.Millisecond) -} diff --git a/docs/results/leader-block-packing/2026-09-13/local-race.txt b/docs/results/leader-block-packing/2026-09-13/local-race.txt deleted file mode 100644 index 1a9f78b60..000000000 --- a/docs/results/leader-block-packing/2026-09-13/local-race.txt +++ /dev/null @@ -1,11 +0,0 @@ -ok github.com/Overclock-Validator/mithril/pkg/accounts 1.305s -ok github.com/Overclock-Validator/mithril/pkg/blockprod 21.817s -ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 1.949s -ok github.com/Overclock-Validator/mithril/pkg/costmodel 2.008s -ok github.com/Overclock-Validator/mithril/pkg/merkletree 2.233s -ok github.com/Overclock-Validator/mithril/pkg/replay 4.644s -ok github.com/Overclock-Validator/mithril/pkg/fees 1.961s -ok github.com/Overclock-Validator/mithril/pkg/turbine 5.114s -ok github.com/Overclock-Validator/mithril/pkg/tpu/txfixture 54.959s -ok github.com/Overclock-Validator/mithril/pkg/config 2.348s -ok github.com/Overclock-Validator/mithril/cmd/mithril/node 3.229s diff --git a/docs/results/leader-block-packing/2026-09-13/local-unit.txt b/docs/results/leader-block-packing/2026-09-13/local-unit.txt deleted file mode 100644 index 0cb4e1052..000000000 --- a/docs/results/leader-block-packing/2026-09-13/local-unit.txt +++ /dev/null @@ -1,10 +0,0 @@ -ok github.com/Overclock-Validator/mithril/pkg/accounts 0.223s -ok github.com/Overclock-Validator/mithril/pkg/blockprod 0.499s -ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 0.593s -ok github.com/Overclock-Validator/mithril/pkg/costmodel 0.816s -ok github.com/Overclock-Validator/mithril/pkg/merkletree 0.625s -ok github.com/Overclock-Validator/mithril/pkg/replay 1.666s -ok github.com/Overclock-Validator/mithril/pkg/fees 0.985s -ok github.com/Overclock-Validator/mithril/pkg/turbine 2.322s -ok github.com/Overclock-Validator/mithril/pkg/config 1.300s -ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.840s diff --git a/docs/results/leader-block-packing/2026-09-13/native-build.txt b/docs/results/leader-block-packing/2026-09-13/native-build.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/results/leader-block-packing/2026-09-13/native-race.txt b/docs/results/leader-block-packing/2026-09-13/native-race.txt deleted file mode 100644 index 30a569aaa..000000000 --- a/docs/results/leader-block-packing/2026-09-13/native-race.txt +++ /dev/null @@ -1,11 +0,0 @@ -ok github.com/Overclock-Validator/mithril/pkg/accounts 1.108s -ok github.com/Overclock-Validator/mithril/pkg/blockprod 14.102s -ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 1.384s -ok github.com/Overclock-Validator/mithril/pkg/costmodel 1.181s -ok github.com/Overclock-Validator/mithril/pkg/merkletree 1.364s -ok github.com/Overclock-Validator/mithril/pkg/replay 2.236s -ok github.com/Overclock-Validator/mithril/pkg/fees 1.135s -ok github.com/Overclock-Validator/mithril/pkg/turbine 3.135s -ok github.com/Overclock-Validator/mithril/pkg/tpu/txfixture 31.532s -ok github.com/Overclock-Validator/mithril/pkg/config 1.007s -ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.181s diff --git a/docs/results/leader-block-packing/2026-09-13/native-vet.txt b/docs/results/leader-block-packing/2026-09-13/native-vet.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt b/docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt deleted file mode 100644 index 8e75ec08d..000000000 --- a/docs/results/leader-block-packing/2026-09-13/round-1-baseline.txt +++ /dev/null @@ -1,7 +0,0 @@ -goos: linux -goarch: amd64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod -cpu: AMD Ryzen 7 9700X 8-Core Processor -BenchmarkReadonlyPairFullBlock/wire-8 3 209635460 ns/op 48622 tx/block 259271528 B/op 3363451 allocs/op -BenchmarkReadonlyPairFullBlock/decoded-8 3 188551991 ns/op 48622 tx/block 237879586 B/op 3071729 allocs/op -PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt b/docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt deleted file mode 100644 index 2b12f04aa..000000000 --- a/docs/results/leader-block-packing/2026-09-13/round-1-candidate.txt +++ /dev/null @@ -1,8 +0,0 @@ -goos: linux -goarch: amd64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod -cpu: AMD Ryzen 7 9700X 8-Core Processor -BenchmarkReadonlyPairFullBlock/wire-8 3 167507945 ns/op 48622 tx/block 223454984 B/op 2728489 allocs/op -BenchmarkReadonlyPairFullBlock/decoded-8 3 129990183 ns/op 48622 tx/block 202061266 B/op 2436756 allocs/op -BenchmarkReadonlyPairPreparedFullBlock-8 3 121710569 ns/op 48622 tx/block 173666002 B/op 1950536 allocs/op -PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt b/docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt deleted file mode 100644 index 9270847f3..000000000 --- a/docs/results/leader-block-packing/2026-09-13/round-2-baseline.txt +++ /dev/null @@ -1,7 +0,0 @@ -goos: linux -goarch: amd64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod -cpu: AMD Ryzen 7 9700X 8-Core Processor -BenchmarkReadonlyPairFullBlock/wire-8 3 206873746 ns/op 48622 tx/block 259272293 B/op 3363455 allocs/op -BenchmarkReadonlyPairFullBlock/decoded-8 3 214317833 ns/op 48622 tx/block 237880618 B/op 3071736 allocs/op -PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt b/docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt deleted file mode 100644 index ba5529459..000000000 --- a/docs/results/leader-block-packing/2026-09-13/round-2-candidate.txt +++ /dev/null @@ -1,8 +0,0 @@ -goos: linux -goarch: amd64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod -cpu: AMD Ryzen 7 9700X 8-Core Processor -BenchmarkReadonlyPairFullBlock/wire-8 3 145202527 ns/op 48622 tx/block 223454946 B/op 2728489 allocs/op -BenchmarkReadonlyPairFullBlock/decoded-8 3 151023757 ns/op 48622 tx/block 202061384 B/op 2436757 allocs/op -BenchmarkReadonlyPairPreparedFullBlock-8 3 101921901 ns/op 48622 tx/block 173666021 B/op 1950536 allocs/op -PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt b/docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt deleted file mode 100644 index ef23596b3..000000000 --- a/docs/results/leader-block-packing/2026-09-13/round-3-baseline.txt +++ /dev/null @@ -1,7 +0,0 @@ -goos: linux -goarch: amd64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod -cpu: AMD Ryzen 7 9700X 8-Core Processor -BenchmarkReadonlyPairFullBlock/wire-8 3 209864927 ns/op 48622 tx/block 259274824 B/op 3363464 allocs/op -BenchmarkReadonlyPairFullBlock/decoded-8 3 189729779 ns/op 48622 tx/block 237880589 B/op 3071738 allocs/op -PASS diff --git a/docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt b/docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt deleted file mode 100644 index a2279a516..000000000 --- a/docs/results/leader-block-packing/2026-09-13/round-3-candidate.txt +++ /dev/null @@ -1,8 +0,0 @@ -goos: linux -goarch: amd64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod -cpu: AMD Ryzen 7 9700X 8-Core Processor -BenchmarkReadonlyPairFullBlock/wire-8 3 150295927 ns/op 48622 tx/block 223455032 B/op 2728489 allocs/op -BenchmarkReadonlyPairFullBlock/decoded-8 3 132400481 ns/op 48622 tx/block 202061368 B/op 2436757 allocs/op -BenchmarkReadonlyPairPreparedFullBlock-8 3 101595082 ns/op 48622 tx/block 173666032 B/op 1950536 allocs/op -PASS diff --git a/docs/results/leader-block-packing/2026-09-13/run-native.sh b/docs/results/leader-block-packing/2026-09-13/run-native.sh deleted file mode 100644 index 8961a3110..000000000 --- a/docs/results/leader-block-packing/2026-09-13/run-native.sh +++ /dev/null @@ -1,31 +0,0 @@ -set -eu -review_dir=/srv/mithril-leader-packing-pr-20260913 -mkdir "$review_dir" -mkdir "$review_dir/candidate" "$review_dir/baseline" "$review_dir/results" -tar -xzf /tmp/mithril-leader-packing-pr-20260913.tar.gz -C "$review_dir/candidate" -tar -xf /tmp/mithril-leader-base-20260913.tar -C "$review_dir/baseline" -cp "$review_dir/candidate/pkg/blockprod/readonly_block_bench_test.go" "$review_dir/baseline/pkg/blockprod/" -cp "$review_dir/candidate/pkg/tpu/txfixture/readonly_pair.go" "$review_dir/baseline/pkg/tpu/txfixture/" -export PATH=/usr/local/go/bin:$PATH -export GOMAXPROCS=8 -systemctl show mithril-alpenglow-pr259.service -p MainPID -p ActiveState > "$review_dir/results/service-before.txt" -sha256sum /tmp/mithril-leader-packing-pr-20260913.tar.gz /tmp/mithril-leader-base-20260913.tar > "$review_dir/results/source-sha256.txt" -lscpu -e=CPU,CORE,SOCKET > "$review_dir/results/topology.txt" -cd "$review_dir/candidate" -nice -n 10 go test -race ./pkg/accounts ./pkg/blockprod/... ./pkg/costmodel ./pkg/merkletree ./pkg/replay ./pkg/fees ./pkg/turbine ./pkg/tpu/txfixture ./pkg/config ./cmd/mithril/node > "$review_dir/results/native-race.txt" 2>&1 -nice -n 10 go vet ./pkg/accounts ./pkg/blockprod/... ./pkg/costmodel ./pkg/merkletree ./pkg/replay ./pkg/fees ./pkg/turbine ./pkg/tpu/txfixture ./pkg/config ./cmd/mithril/node > "$review_dir/results/native-vet.txt" 2>&1 -nice -n 10 go build -o "$review_dir/results/mithril" ./cmd/mithril > "$review_dir/results/native-build.txt" 2>&1 -nice -n 10 go test -c -o "$review_dir/results/candidate.test" ./pkg/blockprod -cd "$review_dir/baseline" -nice -n 10 go test -run '^TestReadonlyPairBlockCapacityAndShredRoundTrip$' ./pkg/blockprod > "$review_dir/results/baseline-capacity.txt" 2>&1 -nice -n 10 go test -c -o "$review_dir/results/baseline.test" ./pkg/blockprod -for round in 1 2 3; do - if [ "$round" = 2 ]; then order='candidate baseline'; else order='baseline candidate'; fi - for variant in $order; do - nice -n 10 taskset -c 0-7 "$review_dir/results/$variant.test" -test.run='^$' -test.bench='^BenchmarkReadonlyPair(FullBlock|PreparedFullBlock)$' -test.benchtime=3x -test.count=1 > "$review_dir/results/round-$round-$variant.txt" 2>&1 - done -done -systemctl show mithril-alpenglow-pr259.service -p MainPID -p ActiveState > "$review_dir/results/service-after.txt" -sha256sum "$review_dir/results/"*.test "$review_dir/results/mithril" > "$review_dir/results/binary-sha256.txt" -cat "$review_dir/results/native-race.txt" -cat "$review_dir/results/round-"*.txt diff --git a/docs/results/leader-block-packing/2026-09-13/service-after.txt b/docs/results/leader-block-packing/2026-09-13/service-after.txt deleted file mode 100644 index bece5e701..000000000 --- a/docs/results/leader-block-packing/2026-09-13/service-after.txt +++ /dev/null @@ -1,2 +0,0 @@ -ActiveState=active -MainPID=3846480 diff --git a/docs/results/leader-block-packing/2026-09-13/service-before.txt b/docs/results/leader-block-packing/2026-09-13/service-before.txt deleted file mode 100644 index bece5e701..000000000 --- a/docs/results/leader-block-packing/2026-09-13/service-before.txt +++ /dev/null @@ -1,2 +0,0 @@ -ActiveState=active -MainPID=3846480 diff --git a/docs/results/leader-block-packing/2026-09-13/source-sha256.txt b/docs/results/leader-block-packing/2026-09-13/source-sha256.txt deleted file mode 100644 index c4996c08f..000000000 --- a/docs/results/leader-block-packing/2026-09-13/source-sha256.txt +++ /dev/null @@ -1,2 +0,0 @@ -c08b4bee669cd754aed39abe4235ab493679ce15ac9f12f928027ea40015c830 /tmp/mithril-leader-packing-pr-20260913.tar.gz -e695e40e720139547b6b6ad5b84ed65ff1d21423874ef352a5bdb681121065ae /tmp/mithril-leader-base-20260913.tar diff --git a/docs/results/leader-block-packing/2026-09-13/summarize.py b/docs/results/leader-block-packing/2026-09-13/summarize.py deleted file mode 100644 index 6ae50531f..000000000 --- a/docs/results/leader-block-packing/2026-09-13/summarize.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Summarize the paired standalone bank benchmarks; run beside the raw logs.""" -import json -from pathlib import Path -from statistics import median - -root = Path(__file__).resolve().parent -samples = {} -for path in sorted(root.glob('round-*.txt')): - variant = path.stem.rsplit('-', 1)[1] - for line in path.read_text().splitlines(): - if not line.startswith('Benchmark'): - continue - fields = line.split() - name = fields[0].rsplit('-', 1)[0] - metrics = dict(zip(fields[3::2], map(float, fields[2::2]))) - samples.setdefault(variant + ':' + name, []).append(metrics) -summary = {key: {metric: median(s[metric] for s in values) for metric in values[0]} - for key, values in sorted(samples.items())} -(root / 'summary.json').write_text(json.dumps({'samples': samples, 'medians': summary}, indent=2) + '\n') -print(json.dumps(summary, indent=2)) diff --git a/docs/results/leader-block-packing/2026-09-13/summary.json b/docs/results/leader-block-packing/2026-09-13/summary.json deleted file mode 100644 index c8bcef79d..000000000 --- a/docs/results/leader-block-packing/2026-09-13/summary.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "samples": { - "baseline:BenchmarkReadonlyPairFullBlock/wire": [ - { - "ns/op": 209635460.0, - "tx/block": 48622.0, - "B/op": 259271528.0, - "allocs/op": 3363451.0 - }, - { - "ns/op": 206873746.0, - "tx/block": 48622.0, - "B/op": 259272293.0, - "allocs/op": 3363455.0 - }, - { - "ns/op": 209864927.0, - "tx/block": 48622.0, - "B/op": 259274824.0, - "allocs/op": 3363464.0 - } - ], - "baseline:BenchmarkReadonlyPairFullBlock/decoded": [ - { - "ns/op": 188551991.0, - "tx/block": 48622.0, - "B/op": 237879586.0, - "allocs/op": 3071729.0 - }, - { - "ns/op": 214317833.0, - "tx/block": 48622.0, - "B/op": 237880618.0, - "allocs/op": 3071736.0 - }, - { - "ns/op": 189729779.0, - "tx/block": 48622.0, - "B/op": 237880589.0, - "allocs/op": 3071738.0 - } - ], - "candidate:BenchmarkReadonlyPairFullBlock/wire": [ - { - "ns/op": 167507945.0, - "tx/block": 48622.0, - "B/op": 223454984.0, - "allocs/op": 2728489.0 - }, - { - "ns/op": 145202527.0, - "tx/block": 48622.0, - "B/op": 223454946.0, - "allocs/op": 2728489.0 - }, - { - "ns/op": 150295927.0, - "tx/block": 48622.0, - "B/op": 223455032.0, - "allocs/op": 2728489.0 - } - ], - "candidate:BenchmarkReadonlyPairFullBlock/decoded": [ - { - "ns/op": 129990183.0, - "tx/block": 48622.0, - "B/op": 202061266.0, - "allocs/op": 2436756.0 - }, - { - "ns/op": 151023757.0, - "tx/block": 48622.0, - "B/op": 202061384.0, - "allocs/op": 2436757.0 - }, - { - "ns/op": 132400481.0, - "tx/block": 48622.0, - "B/op": 202061368.0, - "allocs/op": 2436757.0 - } - ], - "candidate:BenchmarkReadonlyPairPreparedFullBlock": [ - { - "ns/op": 121710569.0, - "tx/block": 48622.0, - "B/op": 173666002.0, - "allocs/op": 1950536.0 - }, - { - "ns/op": 101921901.0, - "tx/block": 48622.0, - "B/op": 173666021.0, - "allocs/op": 1950536.0 - }, - { - "ns/op": 101595082.0, - "tx/block": 48622.0, - "B/op": 173666032.0, - "allocs/op": 1950536.0 - } - ] - }, - "medians": { - "baseline:BenchmarkReadonlyPairFullBlock/decoded": { - "ns/op": 189729779.0, - "tx/block": 48622.0, - "B/op": 237880589.0, - "allocs/op": 3071736.0 - }, - "baseline:BenchmarkReadonlyPairFullBlock/wire": { - "ns/op": 209635460.0, - "tx/block": 48622.0, - "B/op": 259272293.0, - "allocs/op": 3363455.0 - }, - "candidate:BenchmarkReadonlyPairFullBlock/decoded": { - "ns/op": 132400481.0, - "tx/block": 48622.0, - "B/op": 202061368.0, - "allocs/op": 2436757.0 - }, - "candidate:BenchmarkReadonlyPairFullBlock/wire": { - "ns/op": 150295927.0, - "tx/block": 48622.0, - "B/op": 223454984.0, - "allocs/op": 2728489.0 - }, - "candidate:BenchmarkReadonlyPairPreparedFullBlock": { - "ns/op": 101921901.0, - "tx/block": 48622.0, - "B/op": 173666021.0, - "allocs/op": 1950536.0 - } - } -} diff --git a/docs/results/leader-block-packing/2026-09-13/topology.txt b/docs/results/leader-block-packing/2026-09-13/topology.txt deleted file mode 100644 index 4647ea847..000000000 --- a/docs/results/leader-block-packing/2026-09-13/topology.txt +++ /dev/null @@ -1,17 +0,0 @@ -CPU CORE SOCKET - 0 0 0 - 1 1 0 - 2 2 0 - 3 3 0 - 4 4 0 - 5 5 0 - 6 6 0 - 7 7 0 - 8 0 0 - 9 1 0 - 10 2 0 - 11 3 0 - 12 4 0 - 13 5 0 - 14 6 0 - 15 7 0 diff --git a/docs/results/pr-split-2026-09-15/leader/README.md b/docs/results/pr-split-2026-09-15/leader/README.md deleted file mode 100644 index 6fe67c2e5..000000000 --- a/docs/results/pr-split-2026-09-15/leader/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Review branch validation, September 15 - -This PR is split from #279 plus the later working-tree improvements. The tested code commit before this documentation commit was `c10fa0d871caa866c14a85892f8d5947358eb414`. Tests ran locally on Apple M4 Pro, Go1.26.4, GOMAXPROCS3 and package parallelism2. Logs beside this file are the fresh split-branch checks, not native measurements. Existing Zen5 benchmark documents retain their original baselines and scope. No validator restart or deployment occurred during this reorganization. - -Four independent branches start at current alpenglow-dev33dde405. Voting is based on the certificate-processing PR; leader packing is based on the streaming-preparation PR. Runtime changes and status-cache changes are independent. Shared CLI/configuration additions need an ordinary three-file merge reconciliation when combining leader packing and voting. A separate audit checkout reconciled these additions and matched the preserved full implementation exactly across Go sources, module files, TOML configuration and CI. - -The branch-specific race suites and vet passed. The combined audit has a separately documented pre-existing intermittent peer reconnect timeout; this is not reported as an entirely green combined race run. diff --git a/docs/results/pr-split-2026-09-15/leader/leader-tests.log b/docs/results/pr-split-2026-09-15/leader/leader-tests.log deleted file mode 100644 index 639c0e7a4..000000000 --- a/docs/results/pr-split-2026-09-15/leader/leader-tests.log +++ /dev/null @@ -1,10 +0,0 @@ -ok github.com/Overclock-Validator/mithril/pkg/blockprod 21.424s -ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 5.248s -ok github.com/Overclock-Validator/mithril/pkg/accounts 1.281s -ok github.com/Overclock-Validator/mithril/pkg/config 1.157s -ok github.com/Overclock-Validator/mithril/pkg/costmodel 1.512s -ok github.com/Overclock-Validator/mithril/pkg/merkletree 1.452s -ok github.com/Overclock-Validator/mithril/pkg/replay 4.406s -ok github.com/Overclock-Validator/mithril/pkg/tpu/txfixture 53.711s -ok github.com/Overclock-Validator/mithril/pkg/turbine 9.829s -ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.892s diff --git a/docs/results/pr-split-2026-09-15/leader/leader-vet.log b/docs/results/pr-split-2026-09-15/leader/leader-vet.log deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/results/queue-retention/2026-09-14/README.md b/docs/results/queue-retention/2026-09-14/README.md deleted file mode 100644 index ef631bbd8..000000000 --- a/docs/results/queue-retention/2026-09-14/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Bounded scheduler references and regression CI - -Follow-up to PR 279 at `b24de8e616b28eccd1f86c3384a1af96bf9624c0`, with the -queue fix in `667d002f`. This bounds scheduler references and adds regression -CI. The existing slot-local skip scanning/rebuffering policy is unchanged; -`scheduler.go` is identical to the comparison commit. - -## Queue lifetime - -Each entry now records its position in both priority heaps. Consumption, -eviction and expiration remove its counterpart directly, clear the removed -backing-array pointer and release the deduplication-map reference. Returned -transactions retain their owned wire, decoded and prepared data for the caller; -rebuffering inserts exactly one reference in each heap again. Neither priority -ordering, equal-reward FIFO, capacity, fee scoring nor cross-slot retry rules -change. - -The regression keeps one low-reward entry while inserting and consuming 100,000 -higher-reward entries. With capacity 256, the old algorithm retained 100,001 -min-heap references. The candidate finishes with **one active entry and one -reference in each heap**, without requiring a later cleanup sweep. Additional -tests cover symmetric eviction, expiration, repeated rebuffering, new -higher-priority arrivals, retry in a later bank, concurrent operations, exact -reference-model ordering and zeroed slice storage beyond the visible length. - -Both heaps' lengths and map membership equal the number of buffered entries. -Backing-array allocation can retain its high-water capacity, but unused positions -are nil and cannot retain transaction payloads. - -## Validation - -Apple M4 Pro, Go 1.26.4. The scheduler race suite passed with GOMAXPROCS=4. -Scheduler vet and a complete `./cmd/mithril` build also passed. -The exact new CI command also passed locally with GOMAXPROCS=2: - -```sh -GOMAXPROCS=2 go test -race -p 2 -count=1 \ - ./pkg/alpenglow ./pkg/consensus ./pkg/replay \ - ./pkg/turbine ./pkg/sigverify ./pkg/blockprod/... ./cmd/mithril/node -``` - -The new `regression-tests` GitHub job runs these complete package suites, -including reservation/crash recovery, background history ordering, checkpoint -capture and streaming cancellation, alongside the existing build job. This is -not the full repository suite; pre-existing sealevel failures remain documented -in the [combined validation report](../../validator-performance/2026-09-14/README.md). - -Logs: [scheduler race](queue-fixed-race.log), [CI command locally](ci-race-local.log), -[retention result](retention.log). - -## Removal-cost tradeoff - -Three alternating paired rounds of the existing `BenchmarkBufferDrain`, -GOMAXPROCS=1, each selecting all 120,000 transactions from a prefilled queue. -Queue construction and transaction preparation are excluded. Baseline uses the -PR's `b24de8e6` buffer; candidate uses indexed counterpart removal. Both run the -same benchmark. Timing excludes GC costs outside the timed region and does not -measure a sustained validator workload. - -| Priority distribution | Baseline median | Candidate median | -| --- | ---: | ---: | -| Equal rewards / FIFO | 143.0 ns/pop | 188.0 ns/pop | -| Mixed rewards | 254.0 ns/pop | 274.2 ns/pop | - -Both report zero timed allocations per pop. Equal-reward candidate samples were -188.0 / 302.4 / 183.5 ns, so the raw variability should not be hidden behind the -median. These measurements show the direct removal cost, not a net validator -speedup. Memory retention is bounded; the effect on block fullness, garbage -collection and FAST omissions remains unmeasured. No live deployment or -validator restart was performed for this follow-up. - -```sh -GOMAXPROCS=1 go test ./pkg/blockprod/scheduler -run '^$' \ - -bench '^BenchmarkBufferDrain$' -benchtime=120000x -count=3 -``` - -[All sample values](drain-summary.json), [execution order](drain-runs.json), -raw baseline/candidate logs alongside this report. These results are local M4 -measurements and must not be substituted for the earlier Zen 5 bank benchmarks. diff --git a/docs/results/queue-retention/2026-09-14/ci-race-local.log b/docs/results/queue-retention/2026-09-14/ci-race-local.log deleted file mode 100644 index 7fd86596a..000000000 --- a/docs/results/queue-retention/2026-09-14/ci-race-local.log +++ /dev/null @@ -1,8 +0,0 @@ -ok github.com/Overclock-Validator/mithril/pkg/alpenglow 7.643s -ok github.com/Overclock-Validator/mithril/pkg/consensus 9.953s -ok github.com/Overclock-Validator/mithril/pkg/replay 4.578s -ok github.com/Overclock-Validator/mithril/pkg/turbine 9.324s -ok github.com/Overclock-Validator/mithril/pkg/sigverify 23.233s -ok github.com/Overclock-Validator/mithril/pkg/blockprod 21.894s -ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 5.706s -ok github.com/Overclock-Validator/mithril/cmd/mithril/node 1.918s diff --git a/docs/results/queue-retention/2026-09-14/drain-1-baseline.log b/docs/results/queue-retention/2026-09-14/drain-1-baseline.log deleted file mode 100644 index c525bd0d7..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-1-baseline.log +++ /dev/null @@ -1,7 +0,0 @@ -goos: darwin -goarch: arm64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler -cpu: Apple M4 Pro -BenchmarkBufferDrain/equal_rewards 120000 143.0 ns/op 0 B/op 0 allocs/op -BenchmarkBufferDrain/mixed_rewards 120000 240.2 ns/op 0 B/op 0 allocs/op -PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-1-candidate.log b/docs/results/queue-retention/2026-09-14/drain-1-candidate.log deleted file mode 100644 index 426870829..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-1-candidate.log +++ /dev/null @@ -1,7 +0,0 @@ -goos: darwin -goarch: arm64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler -cpu: Apple M4 Pro -BenchmarkBufferDrain/equal_rewards 120000 188.0 ns/op 0 B/op 0 allocs/op -BenchmarkBufferDrain/mixed_rewards 120000 268.0 ns/op 0 B/op 0 allocs/op -PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-2-baseline.log b/docs/results/queue-retention/2026-09-14/drain-2-baseline.log deleted file mode 100644 index 42fe15e61..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-2-baseline.log +++ /dev/null @@ -1,7 +0,0 @@ -goos: darwin -goarch: arm64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler -cpu: Apple M4 Pro -BenchmarkBufferDrain/equal_rewards 120000 147.6 ns/op 0 B/op 0 allocs/op -BenchmarkBufferDrain/mixed_rewards 120000 261.2 ns/op 0 B/op 0 allocs/op -PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-2-candidate.log b/docs/results/queue-retention/2026-09-14/drain-2-candidate.log deleted file mode 100644 index 3aeae2d96..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-2-candidate.log +++ /dev/null @@ -1,7 +0,0 @@ -goos: darwin -goarch: arm64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler -cpu: Apple M4 Pro -BenchmarkBufferDrain/equal_rewards 120000 302.4 ns/op 0 B/op 0 allocs/op -BenchmarkBufferDrain/mixed_rewards 120000 276.0 ns/op 0 B/op 0 allocs/op -PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-3-baseline.log b/docs/results/queue-retention/2026-09-14/drain-3-baseline.log deleted file mode 100644 index 6a332b50f..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-3-baseline.log +++ /dev/null @@ -1,7 +0,0 @@ -goos: darwin -goarch: arm64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler -cpu: Apple M4 Pro -BenchmarkBufferDrain/equal_rewards 120000 140.1 ns/op 0 B/op 0 allocs/op -BenchmarkBufferDrain/mixed_rewards 120000 254.0 ns/op 0 B/op 0 allocs/op -PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-3-candidate.log b/docs/results/queue-retention/2026-09-14/drain-3-candidate.log deleted file mode 100644 index b79128a8e..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-3-candidate.log +++ /dev/null @@ -1,7 +0,0 @@ -goos: darwin -goarch: arm64 -pkg: github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler -cpu: Apple M4 Pro -BenchmarkBufferDrain/equal_rewards 120000 183.5 ns/op 0 B/op 0 allocs/op -BenchmarkBufferDrain/mixed_rewards 120000 274.2 ns/op 0 B/op 0 allocs/op -PASS diff --git a/docs/results/queue-retention/2026-09-14/drain-runs.json b/docs/results/queue-retention/2026-09-14/drain-runs.json deleted file mode 100644 index df2c7f65c..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-runs.json +++ /dev/null @@ -1,80 +0,0 @@ -[ - { - "round": 1, - "variant": "baseline", - "command": [ - "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/baseline-scheduler.test", - "-test.run=^$", - "-test.bench=^BenchmarkBufferDrain$", - "-test.benchtime=120000x", - "-test.count=1" - ], - "exit": 0, - "seconds": 0.6474292278289795 - }, - { - "round": 1, - "variant": "candidate", - "command": [ - "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/candidate-scheduler.test", - "-test.run=^$", - "-test.bench=^BenchmarkBufferDrain$", - "-test.benchtime=120000x", - "-test.count=1" - ], - "exit": 0, - "seconds": 0.6436319351196289 - }, - { - "round": 2, - "variant": "candidate", - "command": [ - "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/candidate-scheduler.test", - "-test.run=^$", - "-test.bench=^BenchmarkBufferDrain$", - "-test.benchtime=120000x", - "-test.count=1" - ], - "exit": 0, - "seconds": 0.306088924407959 - }, - { - "round": 2, - "variant": "baseline", - "command": [ - "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/baseline-scheduler.test", - "-test.run=^$", - "-test.bench=^BenchmarkBufferDrain$", - "-test.benchtime=120000x", - "-test.count=1" - ], - "exit": 0, - "seconds": 0.2681159973144531 - }, - { - "round": 3, - "variant": "baseline", - "command": [ - "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/baseline-scheduler.test", - "-test.run=^$", - "-test.bench=^BenchmarkBufferDrain$", - "-test.benchtime=120000x", - "-test.count=1" - ], - "exit": 0, - "seconds": 0.2570068836212158 - }, - { - "round": 3, - "variant": "candidate", - "command": [ - "/Users/nikolashayes/Desktop/programming/Codex/mithril-pr-review-279-20260914/candidate-scheduler.test", - "-test.run=^$", - "-test.bench=^BenchmarkBufferDrain$", - "-test.benchtime=120000x", - "-test.count=1" - ], - "exit": 0, - "seconds": 0.269057035446167 - } -] diff --git a/docs/results/queue-retention/2026-09-14/drain-summary.json b/docs/results/queue-retention/2026-09-14/drain-summary.json deleted file mode 100644 index 217c079f2..000000000 --- a/docs/results/queue-retention/2026-09-14/drain-summary.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "samples_ns_per_pop": { - "baseline": { - "equal_rewards": [ - 143.0, - 147.6, - 140.1 - ], - "mixed_rewards": [ - 240.2, - 261.2, - 254.0 - ] - }, - "candidate": { - "equal_rewards": [ - 188.0, - 302.4, - 183.5 - ], - "mixed_rewards": [ - 268.0, - 276.0, - 274.2 - ] - } - }, - "median_ns_per_pop": { - "baseline": { - "equal_rewards": 143.0, - "mixed_rewards": 254.0 - }, - "candidate": { - "equal_rewards": 188.0, - "mixed_rewards": 274.2 - } - } -} diff --git a/docs/results/queue-retention/2026-09-14/queue-fixed-race.log b/docs/results/queue-retention/2026-09-14/queue-fixed-race.log deleted file mode 100644 index 5241695b8..000000000 --- a/docs/results/queue-retention/2026-09-14/queue-fixed-race.log +++ /dev/null @@ -1 +0,0 @@ -ok github.com/Overclock-Validator/mithril/pkg/blockprod/scheduler 5.106s diff --git a/docs/results/queue-retention/2026-09-14/retention.log b/docs/results/queue-retention/2026-09-14/retention.log deleted file mode 100644 index 86627651b..000000000 --- a/docs/results/queue-retention/2026-09-14/retention.log +++ /dev/null @@ -1,4 +0,0 @@ -=== RUN TestBufferConsumedEntriesReleaseBothHeapReferences - buffer_retention_test.go:69: capacity=256 active=1 max_refs=1 min_refs=1 ---- PASS: TestBufferConsumedEntriesReleaseBothHeapReferences (0.10s) -PASS From bcc47254562a2e3672aad31b9a07f8a845793a2d Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:39:57 -0500 Subject: [PATCH 7/9] docs: validate archive links and formatting --- docs/leader-packing-evidence.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/leader-packing-evidence.md b/docs/leader-packing-evidence.md index 58be6cac7..8a439d1eb 100644 --- a/docs/leader-packing-evidence.md +++ b/docs/leader-packing-evidence.md @@ -2,7 +2,7 @@ The maintained subsystem documentation and reusable Go benchmarks describe the implementation and reproduction method. Historical raw results and session -notes are retained at [the tested source snapshot](https://github.com/Overclock-Validator/mithril/blob/06ef067798c99947e8cc527450ad28430a9a7333) +notes are retained at [the tested source snapshot](https://github.com/Overclock-Validator/mithril/tree/06ef067798c99947e8cc527450ad28430a9a7333) (tag `review-evidence-20260916-leader-packing`). They are omitted from this proposed merge. [Historical result files](https://github.com/Overclock-Validator/mithril/tree/06ef067798c99947e8cc527450ad28430a9a7333/docs/results) From 3937072475514bcafea9c32d44db4371d2930975 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:42:54 -0500 Subject: [PATCH 8/9] test: use public component serialization in leader round trip --- pkg/blockprod/readonly_block_bench_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/blockprod/readonly_block_bench_test.go b/pkg/blockprod/readonly_block_bench_test.go index b3b8ed713..c605243f5 100644 --- a/pkg/blockprod/readonly_block_bench_test.go +++ b/pkg/blockprod/readonly_block_bench_test.go @@ -116,7 +116,9 @@ func TestReadonlyPairBlockCapacityAndShredRoundTrip(t *testing.T) { count, totalBytes := 0, 0 previous := solana.Hash{0xab} for i, entries := range sink.batches { - raw, err := marshalEntryBatchBytes(entries) + component, err := turbine.NewEntryBatch(entries) + require.NoError(t, err) + raw, err := turbine.MarshalBlockComponent(component) require.NoError(t, err) require.Equal(t, len(raw), sink.bytes[i]) totalBytes += len(raw) From 5ef7a2292af7057a69af4b37b79423aea4a6a21f Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Wed, 16 Sep 2026 00:09:49 -0500 Subject: [PATCH 9/9] leader: precheck buffer admission before transaction preparation --- pkg/blockprod/scheduler/buffer.go | 40 +++++++++++++++++--------- pkg/blockprod/scheduler/buffer_test.go | 25 ++++++++++++++++ pkg/blockprod/scheduler/scheduler.go | 8 ++++-- pkg/costmodel/limits.go | 3 +- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/pkg/blockprod/scheduler/buffer.go b/pkg/blockprod/scheduler/buffer.go index 38bb57a95..2aa5a70c5 100644 --- a/pkg/blockprod/scheduler/buffer.go +++ b/pkg/blockprod/scheduler/buffer.go @@ -129,27 +129,41 @@ const ( InsertRejectedCapacity ) -// Insert adds e when not a duplicate. At capacity, the lowest-reward entry is -// evicted if e has a strictly higher reward; otherwise e is rejected. -func (b *Buffer) Insert(e *entry) (InsertResult, *entry) { - if e == nil || e.tx == nil { - return InsertRejectedCapacity, nil - } +// precheck rejects entries that cannot be admitted now, without reserving space +// or evicting anything. Preparation runs outside mu; Insert must recheck because +// concurrent arrivals or draining can change both duplicates and the floor. +func (b *Buffer) precheck(e *entry) InsertResult { b.mu.Lock() defer b.mu.Unlock() + return b.admissionLocked(e) +} +func (b *Buffer) admissionLocked(e *entry) InsertResult { + if e == nil || e.tx == nil { + return InsertRejectedCapacity + } if _, exists := b.byHash[e.messageHash]; exists { - return InsertDuplicate, nil + return InsertDuplicate } - var evicted *entry if b.alive >= b.capacity { min := b.peekMinAliveLocked() - if min == nil { - return InsertRejectedCapacity, nil - } - if e.reward <= min.reward { - return InsertRejectedCapacity, nil + if min == nil || e.reward <= min.reward { + return InsertRejectedCapacity } + } + return InsertAccepted +} + +// Insert adds e when not a duplicate. At capacity, the lowest-reward entry is +// evicted if e has a strictly higher reward; otherwise e is rejected. +func (b *Buffer) Insert(e *entry) (InsertResult, *entry) { + b.mu.Lock() + defer b.mu.Unlock() + if result := b.admissionLocked(e); result != InsertAccepted { + return result, nil + } + var evicted *entry + if b.alive >= b.capacity { evicted = b.popMinAliveLocked() } b.pushAliveLocked(e) diff --git a/pkg/blockprod/scheduler/buffer_test.go b/pkg/blockprod/scheduler/buffer_test.go index 9b85554ce..6ec46415d 100644 --- a/pkg/blockprod/scheduler/buffer_test.go +++ b/pkg/blockprod/scheduler/buffer_test.go @@ -79,3 +79,28 @@ func TestBufferCleanup(t *testing.T) { require.Equal(t, 1, b.Len()) require.Equal(t, byte(2), b.PopMax().messageHash[0]) } + +func TestBufferPrecheckDoesNotReserveAndInsertRechecks(t *testing.T) { + b := NewBuffer(1) + first := testEntry(10, 1, 1) + require.Equal(t, InsertAccepted, b.precheck(first)) + require.Zero(t, b.Len()) + b.Insert(first) + require.Equal(t, InsertDuplicate, b.precheck(testEntry(20, 2, 1))) + require.Equal(t, InsertRejectedCapacity, b.precheck(testEntry(10, 2, 2))) + candidate := testEntry(20, 3, 3) + require.Equal(t, InsertAccepted, b.precheck(candidate)) + // A precheck must not evict the existing entry, and a later higher-priority + // arrival can reject the candidate even though its precheck succeeded. + require.Equal(t, first, b.PopMax()) + b.Insert(testEntry(30, 4, 4)) + result, evicted := b.Insert(candidate) + require.Equal(t, InsertRejectedCapacity, result) + require.Nil(t, evicted) + b.PopMax() + require.Equal(t, InsertAccepted, b.precheck(candidate)) + b.Insert(testEntry(20, 5, 3)) + result, evicted = b.Insert(candidate) + require.Equal(t, InsertDuplicate, result) + require.Nil(t, evicted) +} diff --git a/pkg/blockprod/scheduler/scheduler.go b/pkg/blockprod/scheduler/scheduler.go index 88e6435bf..9b0864d27 100644 --- a/pkg/blockprod/scheduler/scheduler.go +++ b/pkg/blockprod/scheduler/scheduler.go @@ -173,7 +173,6 @@ func (s *Scheduler) Receive(pkt packet.Packet) { e := &entry{ tx: tx, - prepared: s.preparer.Load().Prepare(tx), wire: wire, wireSize: len(wire), messageHash: messageHash, @@ -181,7 +180,12 @@ func (s *Scheduler) Receive(pkt packet.Packet) { reward: reward, seq: s.seq.Add(1), } - result, evicted := s.buffer.Insert(e) + result := s.buffer.precheck(e) + var evicted *entry + if result == InsertAccepted { + e.prepared = s.preparer.Load().Prepare(tx) + result, evicted = s.buffer.Insert(e) + } s.mu.Lock() switch result { case InsertAccepted: diff --git a/pkg/costmodel/limits.go b/pkg/costmodel/limits.go index 4e3f726ff..26091a3d0 100644 --- a/pkg/costmodel/limits.go +++ b/pkg/costmodel/limits.go @@ -90,7 +90,8 @@ func LimitsForFeatures(feats *features.Features) Limits { return limits } -// LimitsForSlot mirrors Agave v4.3.0-rc.1 runtime/slot_params.rs. A slot-time +// LimitsForSlot mirrors Agave v4.3.0-rc.1 runtime/src/slot_params.rs. +// Reference: https://github.com/anza-xyz/agave/blob/v4.3.0-rc.1/runtime/src/slot_params.rs A slot-time // gate takes effect in the epoch after activation; among effective gates the // shortest duration wins, even if longer-duration gates activate later. func LimitsForSlot(feats *features.Features, schedule *sealevel.SysvarEpochSchedule, slot uint64) (Limits, error) {